diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 96671d3b..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/MLS/.DS_Store b/MLS/.DS_Store deleted file mode 100644 index bd8eac3f..00000000 Binary files a/MLS/.DS_Store and /dev/null differ diff --git a/MLS/Data/Data.xcodeproj/project.pbxproj b/MLS/Data/Data.xcodeproj/project.pbxproj index 1a23f13f..d8c34ba4 100644 --- a/MLS/Data/Data.xcodeproj/project.pbxproj +++ b/MLS/Data/Data.xcodeproj/project.pbxproj @@ -15,7 +15,9 @@ 775D78522E0DA93400DDAD2F /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0879294D2DB90F3A009C301F /* Data.framework */; }; 77660F932DD259AA007A4EF3 /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 77660F922DD259AA007A4EF3 /* KakaoSDKAuth */; }; 77660F952DD259AA007A4EF3 /* KakaoSDKUser in Frameworks */ = {isa = PBXBuildFile; productRef = 77660F942DD259AA007A4EF3 /* KakaoSDKUser */; }; - 776FE2BB2E9FEA930039ACE2 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 776FE2BA2E9FEA930039ACE2 /* RxCocoa */; }; + 77F0E9522EA525CE007368F8 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 77F0E9512EA525CE007368F8 /* RxCocoa */; }; + 77FEEF222EC5B2300023197A /* AuthFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77FEEF202EC5B2300023197A /* AuthFeatureInterface.framework */; }; + 77FEEF242EC5B2300023197A /* DictionaryFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77FEEF212EC5B2300023197A /* DictionaryFeatureInterface.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,6 +38,8 @@ 0879294D2DB90F3A009C301F /* Data.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Data.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 08E4253B2DF17B3700D6ACD3 /* DataMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DataMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 08E425512DF17B5C00D6ACD3 /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 77FEEF202EC5B2300023197A /* AuthFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 77FEEF212EC5B2300023197A /* DictionaryFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DictionaryFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -70,7 +74,9 @@ files = ( 775D78522E0DA93400DDAD2F /* Data.framework in Frameworks */, 08E425562DF17B6300D6ACD3 /* RxSwift in Frameworks */, - 776FE2BB2E9FEA930039ACE2 /* RxCocoa in Frameworks */, + 77F0E9522EA525CE007368F8 /* RxCocoa in Frameworks */, + 77FEEF242EC5B2300023197A /* DictionaryFeatureInterface.framework in Frameworks */, + 77FEEF222EC5B2300023197A /* AuthFeatureInterface.framework in Frameworks */, 08E425522DF17B5C00D6ACD3 /* DomainInterface.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -81,6 +87,8 @@ 084A256D2DB93C1400C395C0 /* Frameworks */ = { isa = PBXGroup; children = ( + 77FEEF202EC5B2300023197A /* AuthFeatureInterface.framework */, + 77FEEF212EC5B2300023197A /* DictionaryFeatureInterface.framework */, 08E425512DF17B5C00D6ACD3 /* DomainInterface.framework */, 080175482DCD274B00D0919F /* DomainInterface.framework */, 084A25C72DB93E0500C395C0 /* Core.framework */, @@ -175,7 +183,7 @@ name = DataMock; packageProductDependencies = ( 08E425552DF17B6300D6ACD3 /* RxSwift */, - 776FE2BA2E9FEA930039ACE2 /* RxCocoa */, + 77F0E9512EA525CE007368F8 /* RxCocoa */, ); productName = DataMock; productReference = 08E4253B2DF17B3700D6ACD3 /* DataMock.framework */; @@ -633,7 +641,7 @@ package = 77660F912DD259AA007A4EF3 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */; productName = KakaoSDKUser; }; - 776FE2BA2E9FEA930039ACE2 /* RxCocoa */ = { + 77F0E9512EA525CE007368F8 /* RxCocoa */ = { isa = XCSwiftPackageProductDependency; package = 0858ABFE2DCFDC340060EBCA /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxCocoa; diff --git a/MLS/Data/Data/Network/DTO/AuthDTO/MemberDTO.swift b/MLS/Data/Data/Network/DTO/AuthDTO/MemberDTO.swift index ba381c0a..5126bcd5 100644 --- a/MLS/Data/Data/Network/DTO/AuthDTO/MemberDTO.swift +++ b/MLS/Data/Data/Network/DTO/AuthDTO/MemberDTO.swift @@ -20,7 +20,10 @@ public struct MemberDTO: Decodable { jobName: "", level: level, profileUrl: profileImageUrl, - platform: provider == "APPLE" ? .apple : .kakao + platform: provider == "APPLE" ? .apple : .kakao, + noticeAgreement: noticeAgreement, + patchNoteAgreement: patchNoteAgreement, + eventAgreement: eventAgreement ) } diff --git a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift index 8819e047..75e93b06 100644 --- a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift +++ b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift @@ -16,6 +16,20 @@ public struct DictionaryDetailItemResponseDTO: Decodable { public let bookmarkId: Int? public func toDomain() -> DictionaryDetailItemResponse { - return DictionaryDetailItemResponse(itemId: itemId, nameKr: nameKr, nameEn: nameEn, descriptionText: descriptionText, imgUrl: imgUrl, npcPrice: npcPrice, itemType: itemType, categoryHierachy: categoryHierachy, availableJobs: availableJobs, requiredStats: requiredStats, equipmentStats: equipmentStats, scrollDetail: scrollDetail, bookmarkId: bookmarkId) + return DictionaryDetailItemResponse( + itemId: itemId, + nameKr: nameKr, + nameEn: nameEn, + descriptionText: descriptionText, + imgUrl: imgUrl, + npcPrice: npcPrice, + itemType: itemType, + categoryHierachy: categoryHierachy, + availableJobs: availableJobs, + requiredStats: requiredStats, + equipmentStats: equipmentStats, + scrollDetail: scrollDetail, + bookmarkId: bookmarkId + ) } } diff --git a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Monster/DictionaryDetailMonsterResponseDTO.swift b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Monster/DictionaryDetailMonsterResponseDTO.swift index 125e5f23..b094b332 100644 --- a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Monster/DictionaryDetailMonsterResponseDTO.swift +++ b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Monster/DictionaryDetailMonsterResponseDTO.swift @@ -20,6 +20,24 @@ public struct DictionaryDetailMonsterResponseDTO: Decodable { public let bookmarkId: Int? public func toDomain() -> DictionaryDetailMonsterResponse { - return DictionaryDetailMonsterResponse(monsterId: monsterId, nameKr: nameKr, nameEn: nameEn, imageUrl: imageUrl, level: level, exp: exp, hp: hp, mp: mp, physicalDefense: physicalDefense, magicDefense: magicDefense, requiredAccuracy: requiredAccuracy, bonusAccuracyPerLevelLower: bonusAccuracyPerLevelLower, evasionRate: evasionRate, mesoDropAmount: mesoDropAmount, mesoDropRate: mesoDropRate, typeEffectiveness: typeEffectiveness, bookmarkId: bookmarkId) + return DictionaryDetailMonsterResponse( + monsterId: monsterId, + nameKr: nameKr, + nameEn: nameEn, + imageUrl: imageUrl, + level: level, + exp: exp, + hp: hp, + mp: mp, + physicalDefense: physicalDefense, + magicDefense: magicDefense, + requiredAccuracy: requiredAccuracy, + bonusAccuracyPerLevelLower: bonusAccuracyPerLevelLower, + evasionRate: evasionRate, + mesoDropAmount: mesoDropAmount, + mesoDropRate: mesoDropRate, + typeEffectiveness: typeEffectiveness, + bookmarkId: bookmarkId + ) } } diff --git a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Quest/DictionaryDetailQuestResponseDTO.swift b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Quest/DictionaryDetailQuestResponseDTO.swift index 52bec217..2376ce48 100644 --- a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Quest/DictionaryDetailQuestResponseDTO.swift +++ b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Quest/DictionaryDetailQuestResponseDTO.swift @@ -21,6 +21,25 @@ public struct DictionaryDetailQuestResponseDTO: Decodable { public let bookmarkId: Int? public func toDomain() -> DictionaryDetailQuestResponse { - return DictionaryDetailQuestResponse(questId: questId, titlePrefix: titlePrefix, nameKr: nameKr, nameEn: nameEn, iconUrl: iconUrl, questType: questType, minLevel: minLevel, maxLevel: maxLevel, requiredMesoStart: requiredMesoStart, startNpcId: startNpcId, startNpcName: startNpcName, endNpcId: endNpcId, endNpcName: endNpcName, reward: reward, rewardItems: rewardItems, requirements: requirements, allowedJobs: allowedJobs, bookmarkId: bookmarkId) + return DictionaryDetailQuestResponse( + questId: questId, + titlePrefix: titlePrefix, + nameKr: nameKr, + nameEn: nameEn, + iconUrl: iconUrl, + questType: questType, + minLevel: minLevel, + maxLevel: maxLevel, + requiredMesoStart: requiredMesoStart, + startNpcId: startNpcId, + startNpcName: startNpcName, + endNpcId: endNpcId, + endNpcName: endNpcName, + reward: reward, + rewardItems: rewardItems, + requirements: requirements, + allowedJobs: allowedJobs, + bookmarkId: bookmarkId + ) } } diff --git a/MLS/Data/Data/Network/Endpoints/AlarmEndPoint.swift b/MLS/Data/Data/Network/Endpoints/AlarmEndPoint.swift index ad39ec39..721edf77 100644 --- a/MLS/Data/Data/Network/Endpoints/AlarmEndPoint.swift +++ b/MLS/Data/Data/Network/Endpoints/AlarmEndPoint.swift @@ -42,7 +42,7 @@ public enum AlarmEndPoint { public static func fetchAll(query: Encodable) -> ResponsableEndPoint { .init( baseURL: base, - path: "/api/v1/alrim/list/all", + path: "/api/v1/alrim/all", method: .GET, query: query ) diff --git a/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift b/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift index c79b48f3..0a482051 100644 --- a/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift @@ -44,7 +44,7 @@ public class AlarmAPIRepositoryImpl: AlarmAPIRepository { } public func setRead(alarmLink: String) -> Completable { - let endpoint = AlarmEndPoint.setRead(query: setReadQuery(alrimLink: alarmLink)) + let endpoint = AlarmEndPoint.setRead(query: SetReadQuery(alrimLink: alarmLink)) return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor) } @@ -56,7 +56,7 @@ private extension AlarmAPIRepositoryImpl { let pageSize: Int } - struct setReadQuery: Encodable { + struct SetReadQuery: Encodable { let alrimLink: String } } diff --git a/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift b/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift index 9f668321..6ff36b5b 100644 --- a/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift @@ -13,7 +13,7 @@ public class AuthAPIRepositoryImpl: AuthAPIRepository { self.tokenInterceptor = interceptor } - public func fetchProfile() -> Observable { + public func fetchProfile() -> Observable { let endpoint = AuthEndPoint.fetchProfile() return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor) .map { $0.toMyPageDomain() } @@ -109,9 +109,10 @@ public class AuthAPIRepositoryImpl: AuthAPIRepository { return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor) } - public func updateNickName(nickName: String) -> Completable { + public func updateNickName(nickName: String) -> Observable { let endPoint = AuthEndPoint.updateNickName(body: NickNameBody(nickname: nickName)) return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor) + .map { $0.toMyPageDomain() } } public func updateProfileImage(url: String) -> Completable { diff --git a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift index 39d6a334..f0e8f118 100644 --- a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift @@ -4,13 +4,14 @@ import Foundation import RxSwift public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { - private let key = "recentSearch" + private let recentSearchkey = "recentSearch" + private let platformKey = "platformKey" - public init() { } + public init() {} public func fetchRecentSearch() -> Observable<[String]> { return Observable.create { observer in - let current = UserDefaults.standard.stringArray(forKey: self.key) ?? [] + let current = UserDefaults.standard.stringArray(forKey: self.recentSearchkey) ?? [] observer.onNext(current) observer.onCompleted() return Disposables.create() @@ -19,13 +20,13 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { public func addRecentSearch(keyword: String) -> Completable { return Completable.create { completable in - var current = UserDefaults.standard.stringArray(forKey: self.key) ?? [] + var current = UserDefaults.standard.stringArray(forKey: self.recentSearchkey) ?? [] // 중복 제거 - current.removeAll(where: { $0 == keyword}) + current.removeAll(where: { $0 == keyword }) current.insert(keyword, at: 0) - UserDefaults.standard.set(current, forKey: self.key) + UserDefaults.standard.set(current, forKey: self.recentSearchkey) completable(.completed) return Disposables.create() } @@ -33,16 +34,37 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { public func removeRecentSearch(keyword: String) -> Completable { return Completable.create { completable in - var current = UserDefaults.standard.stringArray(forKey: self.key) ?? [] + var current = UserDefaults.standard.stringArray(forKey: self.recentSearchkey) ?? [] - // 해당 키워드 제거 - current.removeAll { $0 == keyword } + // 해당 키워드 제거 + current.removeAll { $0 == keyword } - // 다시 저장 - UserDefaults.standard.set(current, forKey: self.key) + // 다시 저장 + UserDefaults.standard.set(current, forKey: self.recentSearchkey) - completable(.completed) - return Disposables.create() + completable(.completed) + return Disposables.create() + } + } + + public func fetchPlatform() -> Observable { + return Observable.create { observer in + if let rawValue = UserDefaults.standard.string(forKey: self.platformKey), + let platform = LoginPlatform(rawValue: rawValue) { + observer.onNext(platform) + } else { + observer.onNext(nil) } + observer.onCompleted() + return Disposables.create() + } + } + + public func savePlatform(platform: LoginPlatform) -> Completable { + return Completable.create { completable in + UserDefaults.standard.set(platform.rawValue, forKey: self.platformKey) + completable(.completed) + return Disposables.create() + } } } diff --git a/MLS/Data/DataMock/Factory/LoginFactoryMock.swift b/MLS/Data/DataMock/Factory/LoginFactoryMock.swift index 69618f51..1bb84a2d 100644 --- a/MLS/Data/DataMock/Factory/LoginFactoryMock.swift +++ b/MLS/Data/DataMock/Factory/LoginFactoryMock.swift @@ -6,9 +6,14 @@ import DomainInterface public final class LoginFactoryMock: LoginFactory { public init() {} - public func make(isReLogin isRelogin: Bool) -> BaseViewController { + public func make(exitRoute: LoginExitRoute) -> BaseViewController { let viewController = BaseViewController() viewController.view.backgroundColor = .blue return viewController } + + public func make(exitRoute: LoginExitRoute, onLoginCompleted: (() -> Void)?) -> BaseViewController { + return BaseViewController() + } + } diff --git a/MLS/Data/DataMock/Repository/AuthAPIRepositoryMock.swift b/MLS/Data/DataMock/Repository/AuthAPIRepositoryMock.swift index 6a5b8bd7..587b54f9 100644 --- a/MLS/Data/DataMock/Repository/AuthAPIRepositoryMock.swift +++ b/MLS/Data/DataMock/Repository/AuthAPIRepositoryMock.swift @@ -6,6 +6,10 @@ import DomainInterface import RxSwift public class AuthAPIRepositoryMock: AuthAPIRepository { + public func fetchProfile() -> Observable { + return .empty() + } + public func fetchJob(jobId: String) -> Observable { return .empty() } @@ -94,7 +98,7 @@ public class AuthAPIRepositoryMock: AuthAPIRepository { return .empty() } - public func updateNickName(nickName: String) -> RxSwift.Completable { + public func updateNickName(nickName: String) -> Observable { return .empty() } } diff --git a/MLS/Domain/Domain/UseCaseImpl/Alarm/FetchAllUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/Alarm/FetchAllAlarmUseCaseImpl.swift similarity index 86% rename from MLS/Domain/Domain/UseCaseImpl/Alarm/FetchAllUseCaseImpl.swift rename to MLS/Domain/Domain/UseCaseImpl/Alarm/FetchAllAlarmUseCaseImpl.swift index b1258d03..60f51b83 100644 --- a/MLS/Domain/Domain/UseCaseImpl/Alarm/FetchAllUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/Alarm/FetchAllAlarmUseCaseImpl.swift @@ -4,7 +4,7 @@ import DomainInterface import RxSwift -public class FetchAllUseCaseImpl: FetchAllUseCase { +public class FetchAllAlarmUseCaseImpl: FetchAllAlarmUseCase { private var repository: AlarmAPIRepository public init(repository: AlarmAPIRepository) { diff --git a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/FetchPlatformUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/FetchPlatformUseCaseImpl.swift new file mode 100644 index 00000000..40e7100d --- /dev/null +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/FetchPlatformUseCaseImpl.swift @@ -0,0 +1,17 @@ +import Foundation + +import DomainInterface + +import RxSwift + +public class FetchPlatformUseCaseImpl: FetchPlatformUseCase { + private var repository: UserDefaultsRepository + + public init(repository: UserDefaultsRepository) { + self.repository = repository + } + + public func execute() -> Observable { + return repository.fetchPlatform() + } +} diff --git a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithAppleUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithAppleUseCaseImpl.swift index 39d5ff9c..fbff7c74 100644 --- a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithAppleUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithAppleUseCaseImpl.swift @@ -5,15 +5,33 @@ import DomainInterface import RxSwift public class LoginWithAppleUseCaseImpl: LoginWithAppleUseCase { - private var repository: AuthAPIRepository + private var authRepository: AuthAPIRepository + private let tokenRepository: TokenRepository + private var userDefaultsRepository: UserDefaultsRepository - public init(repository: AuthAPIRepository) { - self.repository = repository + public init(authRepository: AuthAPIRepository, tokenRepository: TokenRepository, userDefaultsRepository: UserDefaultsRepository) { + self.authRepository = authRepository + self.tokenRepository = tokenRepository + self.userDefaultsRepository = userDefaultsRepository } // 로그인할때 토큰 저장 필요 public func execute(credential: Credential) -> Observable { - return repository.loginWithApple(credential: credential) + return authRepository.loginWithApple(credential: credential) + .flatMap { response -> Observable in + let saveAccess = self.tokenRepository.saveToken(type: .accessToken, value: response.accessToken) + let saveRefresh = self.tokenRepository.saveToken(type: .refreshToken, value: response.refreshToken) + let savePlatform = self.userDefaultsRepository.savePlatform(platform: .apple) + + switch (saveAccess, saveRefresh) { + case (.success, .success): + return savePlatform.andThen(Observable.just(response)) + default: + return Observable.error( + TokenRepositoryError.dataConversionError(message: "Failed to save tokens") + ) + } + } .catch { error in Observable.error(error) } diff --git a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift index 776921e0..6912e4b6 100644 --- a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift @@ -5,15 +5,34 @@ import DomainInterface import RxSwift public class LoginWithKakaoUseCaseImpl: LoginWithKakaoUseCase { - private var repository: AuthAPIRepository + private var authRepository: AuthAPIRepository + private let tokenRepository: TokenRepository + private var userDefaultsRepository: UserDefaultsRepository - public init(repository: AuthAPIRepository) { - self.repository = repository + public init(authRepository: AuthAPIRepository, tokenRepository: TokenRepository, userDefaultsRepository: UserDefaultsRepository) { + self.authRepository = authRepository + self.tokenRepository = tokenRepository + self.userDefaultsRepository = userDefaultsRepository } // 로그인할때 토큰 저장 필요 public func execute(credential: Credential) -> Observable { - return repository.loginWithKakao(credential: credential) + return authRepository.loginWithKakao(credential: credential) + .flatMap { response -> Observable in + let saveAccess = self.tokenRepository.saveToken(type: .accessToken, value: response.accessToken) + let saveRefresh = self.tokenRepository.saveToken(type: .refreshToken, value: response.refreshToken) + let savePlatform = self.userDefaultsRepository.savePlatform(platform: .apple) + + // ✅ 모든 저장 결과 확인 + switch (saveAccess, saveRefresh) { + case (.success, .success): + return savePlatform.andThen(Observable.just(response)) + default: + return Observable.error( + TokenRepositoryError.dataConversionError(message: "Failed to save tokens") + ) + } + } .catch { error in Observable.error(error) } diff --git a/MLS/Domain/Domain/UseCaseImpl/DictionaryNotification/FetchNotificationUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/DictionaryNotification/FetchNotificationUseCaseImpl.swift deleted file mode 100644 index 396e57ec..00000000 --- a/MLS/Domain/Domain/UseCaseImpl/DictionaryNotification/FetchNotificationUseCaseImpl.swift +++ /dev/null @@ -1,19 +0,0 @@ -import DomainInterface - -import RxSwift - -public class FetchNotificationUseCaseImpl: FetchNotificationUseCase { - public init() {} - - public func execute() -> Observable<[Notification]> { - return Observable.just([ - Notification(title: "신규 업데이트 알림", date: "2025년 1월 1일"), - Notification(title: "신규 업데이트 알림", date: "2025년 2월 1일", isChecked: true), - Notification(title: "신규 업데이트 알림", date: "2025년 3월 1일"), - Notification(title: "신규 업데이트 알림", date: "2025년 4월 1일"), - Notification(title: "신규 업데이트 알림", date: "2025년 5월 1일", isChecked: true), - Notification(title: "신규 업데이트 알림", date: "2025년 6월 1일"), - Notification(title: "신규 업데이트 알림", date: "2025년 7월 1일") - ]) - } -} diff --git a/MLS/Domain/Domain/UseCaseImpl/MyPage/FetchProfileUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/MyPage/FetchProfileUseCaseImpl.swift index ae7936d8..6353a869 100644 --- a/MLS/Domain/Domain/UseCaseImpl/MyPage/FetchProfileUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/MyPage/FetchProfileUseCaseImpl.swift @@ -11,17 +11,17 @@ public class FetchProfileUseCaseImpl: FetchProfileUseCase { self.fetchJobUseCase = fetchJobUseCase } - public func execute() -> Observable { + public func execute() -> Observable { return repository.fetchProfile() - .flatMap { [weak self] profile -> Observable in - guard let self = self, let jobId = profile.jobId else { + .flatMap { [weak self] profile -> Observable in + guard let self = self, let jobId = profile?.jobId else { return .just(profile) } return self.fetchJobUseCase.execute(jobId: String(jobId)) .map { job in var new = profile - new.jobName = job.name + new?.jobName = job.name return new } } diff --git a/MLS/Domain/Domain/UseCaseImpl/MyPage/UpdateNickNameUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/MyPage/UpdateNickNameUseCaseImpl.swift index e8d5fd25..64fabd1e 100644 --- a/MLS/Domain/Domain/UseCaseImpl/MyPage/UpdateNickNameUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/MyPage/UpdateNickNameUseCaseImpl.swift @@ -9,7 +9,7 @@ public class UpdateNickNameUseCaseImpl: UpdateNickNameUseCase { self.repository = repository } - public func execute(nickName: String) -> Completable { + public func execute(nickName: String) -> Observable { return repository.updateNickName(nickName: nickName) } } diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailItemResponse.swift b/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailItemResponse.swift index acb54a0c..5ef9c4ff 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailItemResponse.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailItemResponse.swift @@ -13,7 +13,21 @@ public struct DictionaryDetailItemResponse: Equatable { public let scrollDetail: ScrollDetail? // 주문서 상세정보 public let bookmarkId: Int? - public init(itemId: Int?, nameKr: String?, nameEn: String?, descriptionText: String?, imgUrl: String?, npcPrice: Int?, itemType: String?, categoryHierachy: CategoryHierachy?, availableJobs: [Jobs]?, requiredStats: RequiredStats?, equipmentStats: EquipmentStats?, scrollDetail: ScrollDetail?, bookmarkId: Int?) { + public init( + itemId: Int?, + nameKr: String?, + nameEn: String?, + descriptionText: String?, + imgUrl: String?, + npcPrice: Int?, + itemType: String?, + categoryHierachy: CategoryHierachy?, + availableJobs: [Jobs]?, + requiredStats: RequiredStats?, + equipmentStats: EquipmentStats?, + scrollDetail: ScrollDetail?, + bookmarkId: Int? + ) { self.itemId = itemId self.nameKr = nameKr self.nameEn = nameEn diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailMonsterResponse.swift b/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailMonsterResponse.swift index e4a4d5cb..751f1b17 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailMonsterResponse.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailMonsterResponse.swift @@ -21,7 +21,25 @@ public struct DictionaryDetailMonsterResponse: Equatable { public let typeEffectiveness: Effectiveness? public let bookmarkId: Int? - public init(monsterId: Int, nameKr: String, nameEn: String, imageUrl: String, level: Int, exp: Int, hp: Int, mp: Int, physicalDefense: Int, magicDefense: Int, requiredAccuracy: Int, bonusAccuracyPerLevelLower: Double, evasionRate: Int, mesoDropAmount: Int?, mesoDropRate: Int?, typeEffectiveness: Effectiveness?, bookmarkId: Int?) { + public init( + monsterId: Int, + nameKr: String, + nameEn: String, + imageUrl: String, + level: Int, + exp: Int, + hp: Int, + mp: Int, + physicalDefense: Int, + magicDefense: Int, + requiredAccuracy: Int, + bonusAccuracyPerLevelLower: Double, + evasionRate: Int, + mesoDropAmount: Int?, + mesoDropRate: Int?, + typeEffectiveness: Effectiveness?, + bookmarkId: Int? + ) { self.monsterId = monsterId self.nameKr = nameKr self.nameEn = nameEn diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailQuestResponse.swift b/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailQuestResponse.swift index 4a58538e..753fbc21 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailQuestResponse.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryDetail/DictionaryDetailQuestResponse.swift @@ -18,7 +18,26 @@ public struct DictionaryDetailQuestResponse: Equatable { public let allowedJobs: [AllowedJob]? public let bookmarkId: Int? - public init(questId: Int?, titlePrefix: String?, nameKr: String?, nameEn: String?, iconUrl: String?, questType: String?, minLevel: Int?, maxLevel: Int?, requiredMesoStart: Int?, startNpcId: Int?, startNpcName: String?, endNpcId: Int?, endNpcName: String?, reward: Reward?, rewardItems: [RewardItem]?, requirements: [Requirements]?, allowedJobs: [AllowedJob]?, bookmarkId: Int?) { + public init( + questId: Int?, + titlePrefix: String?, + nameKr: String?, + nameEn: String?, + iconUrl: String?, + questType: String?, + minLevel: Int?, + maxLevel: Int?, + requiredMesoStart: Int?, + startNpcId: Int?, + startNpcName: String?, + endNpcId: Int?, + endNpcName: String?, + reward: Reward?, + rewardItems: [RewardItem]?, + requirements: [Requirements]?, + allowedJobs: [AllowedJob]?, + bookmarkId: Int? + ) { self.questId = questId self.titlePrefix = titlePrefix self.nameKr = nameKr diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionnaryItemType.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionnaryItemType.swift index c7fa6a13..3ea6cc2e 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionnaryItemType.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionnaryItemType.swift @@ -37,15 +37,19 @@ public enum DictionaryItemType: String { } } - static func from(_ string: String) -> DictionaryItemType? { - let mapping: [String: DictionaryItemType] = [ - "item": .item, - "monster": .monster, - "map": .map, - "npc": .npc, - "quest": .quest - ] - return mapping[string.lowercased()] + public var toDictionaryType: DictionaryType? { + switch self { + case .item: + return .item + case .monster: + return .monster + case .map: + return .map + case .npc: + return .npc + case .quest: + return .quest + } } } diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryNotification/Notification.swift b/MLS/Domain/DomainInterface/Entity/DictionaryNotification/Notification.swift deleted file mode 100644 index 9821b1c0..00000000 --- a/MLS/Domain/DomainInterface/Entity/DictionaryNotification/Notification.swift +++ /dev/null @@ -1,11 +0,0 @@ -public struct Notification { - public let title: String - public let date: String - public let isChecked: Bool - - public init(title: String, date: String, isChecked: Bool = false) { - self.title = title - self.date = date - self.isChecked = isChecked - } -} diff --git a/MLS/Domain/DomainInterface/Entity/MyPage/MyPageResponse.swift b/MLS/Domain/DomainInterface/Entity/MyPage/MyPageResponse.swift index d1698674..05fed33d 100644 --- a/MLS/Domain/DomainInterface/Entity/MyPage/MyPageResponse.swift +++ b/MLS/Domain/DomainInterface/Entity/MyPage/MyPageResponse.swift @@ -1,17 +1,23 @@ -public struct MyPageResponse { - public let nickname: String +public struct MyPageResponse: Equatable { + public var nickname: String public let jobId: Int? public var jobName: String public let level: Int? public let profileUrl: String public let platform: LoginPlatform + public let noticeAgreement: Bool + public let patchNoteAgreement: Bool + public let eventAgreement: Bool - public init(nickname: String, jobId: Int?, jobName: String, level: Int?, profileUrl: String, platform: LoginPlatform) { + public init(nickname: String, jobId: Int?, jobName: String, level: Int?, profileUrl: String, platform: LoginPlatform, noticeAgreement: Bool?, patchNoteAgreement: Bool?, eventAgreement: Bool?) { self.nickname = nickname self.jobId = jobId self.jobName = jobName self.level = level self.profileUrl = profileUrl self.platform = platform + self.noticeAgreement = noticeAgreement ?? false + self.patchNoteAgreement = patchNoteAgreement ?? false + self.eventAgreement = eventAgreement ?? false } } diff --git a/MLS/Domain/DomainInterface/Entity/Shared/LoginPlatform.swift b/MLS/Domain/DomainInterface/Entity/Shared/LoginPlatform.swift index 835cd2df..35c4f7ff 100644 --- a/MLS/Domain/DomainInterface/Entity/Shared/LoginPlatform.swift +++ b/MLS/Domain/DomainInterface/Entity/Shared/LoginPlatform.swift @@ -1,6 +1,6 @@ import Foundation -public enum LoginPlatform { +public enum LoginPlatform: String { case kakao case apple } diff --git a/MLS/Domain/DomainInterface/Repository/AuthAPIRepository.swift b/MLS/Domain/DomainInterface/Repository/AuthAPIRepository.swift index adc7d437..1b0b28c6 100644 --- a/MLS/Domain/DomainInterface/Repository/AuthAPIRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/AuthAPIRepository.swift @@ -60,9 +60,9 @@ public protocol AuthAPIRepository { func updateNotificationAgreement(noticeAgreement: Bool, patchNoteAgreement: Bool, eventAgreement: Bool) -> Completable - func updateNickName(nickName: String) -> Completable + func updateNickName(nickName: String) -> Observable func updateProfileImage(url: String) -> Completable - func fetchProfile() -> Observable + func fetchProfile() -> Observable } diff --git a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift index 2f4a3a32..b469b8c1 100644 --- a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift @@ -4,4 +4,7 @@ public protocol UserDefaultsRepository { func fetchRecentSearch() -> Observable<[String]> func addRecentSearch(keyword: String) -> Completable func removeRecentSearch(keyword: String) -> Completable + + func fetchPlatform() -> Observable + func savePlatform(platform: LoginPlatform) -> Completable } diff --git a/MLS/Domain/DomainInterface/UseCase/Alarm/FetchAllUseCase.swift b/MLS/Domain/DomainInterface/UseCase/Alarm/FetchAllAlarmUseCase.swift similarity index 74% rename from MLS/Domain/DomainInterface/UseCase/Alarm/FetchAllUseCase.swift rename to MLS/Domain/DomainInterface/UseCase/Alarm/FetchAllAlarmUseCase.swift index f62d902c..72a34087 100644 --- a/MLS/Domain/DomainInterface/UseCase/Alarm/FetchAllUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/Alarm/FetchAllAlarmUseCase.swift @@ -1,5 +1,5 @@ import RxSwift -public protocol FetchAllUseCase { +public protocol FetchAllAlarmUseCase { func execute(cursor: [Int]?, pageSize: Int) -> Observable> } diff --git a/MLS/Domain/DomainInterface/UseCase/AuthAPI/FetchPlatformUseCase.swift b/MLS/Domain/DomainInterface/UseCase/AuthAPI/FetchPlatformUseCase.swift new file mode 100644 index 00000000..ce1113f9 --- /dev/null +++ b/MLS/Domain/DomainInterface/UseCase/AuthAPI/FetchPlatformUseCase.swift @@ -0,0 +1,5 @@ +import RxSwift + +public protocol FetchPlatformUseCase { + func execute() -> Observable +} diff --git a/MLS/Domain/DomainInterface/UseCase/DictionaryNotification/FetchNotificationUseCase.swift b/MLS/Domain/DomainInterface/UseCase/DictionaryNotification/FetchNotificationUseCase.swift deleted file mode 100644 index f7e47f12..00000000 --- a/MLS/Domain/DomainInterface/UseCase/DictionaryNotification/FetchNotificationUseCase.swift +++ /dev/null @@ -1,5 +0,0 @@ -import RxSwift - -public protocol FetchNotificationUseCase { - func execute() -> Observable<[Notification]> -} diff --git a/MLS/Domain/DomainInterface/UseCase/MyPage/FetchProfileUseCase.swift b/MLS/Domain/DomainInterface/UseCase/MyPage/FetchProfileUseCase.swift index e97b5124..f8176cb4 100644 --- a/MLS/Domain/DomainInterface/UseCase/MyPage/FetchProfileUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/MyPage/FetchProfileUseCase.swift @@ -1,5 +1,5 @@ import RxSwift public protocol FetchProfileUseCase { - func execute() -> Observable + func execute() -> Observable } diff --git a/MLS/Domain/DomainInterface/UseCase/MyPage/UpdateNickNameUseCase.swift b/MLS/Domain/DomainInterface/UseCase/MyPage/UpdateNickNameUseCase.swift index 2eca5651..19638955 100644 --- a/MLS/Domain/DomainInterface/UseCase/MyPage/UpdateNickNameUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/MyPage/UpdateNickNameUseCase.swift @@ -1,5 +1,5 @@ import RxSwift public protocol UpdateNickNameUseCase { - func execute(nickName: String) -> Completable + func execute(nickName: String) -> Observable } diff --git a/MLS/MLS/Application/AppCoordinator.swift b/MLS/MLS/Application/AppCoordinator.swift new file mode 100644 index 00000000..520823cf --- /dev/null +++ b/MLS/MLS/Application/AppCoordinator.swift @@ -0,0 +1,69 @@ +import AuthFeature +import AuthFeatureInterface +import BaseFeature +import BookmarkFeatureInterface +import DesignSystem +import DictionaryFeatureInterface +import MyPageFeatureInterface +import UIKit + +import RxSwift + +public final class AppCoordinator { + // MARK: - Properties + private let window: UIWindow + private let dictionaryMainViewFactory: DictionaryMainViewFactory + private let bookmarkMainFactory: BookmarkMainFactory + private let myPageMainFactory: MyPageMainFactory + private let loginFactory: LoginFactory + + private let disposeBag = DisposeBag() + + // MARK: - Init + public init( + window: UIWindow, + dictionaryMainViewFactory: DictionaryMainViewFactory, + bookmarkMainFactory: BookmarkMainFactory, + myPageMainFactory: MyPageMainFactory, + loginFactory: LoginFactory + ) { + self.window = window + self.dictionaryMainViewFactory = dictionaryMainViewFactory + self.bookmarkMainFactory = bookmarkMainFactory + self.myPageMainFactory = myPageMainFactory + self.loginFactory = loginFactory + } + + // MARK: - Public Methods + public func showMainTab() { + let tabBar = BottomTabBarController(viewControllers: [ + dictionaryMainViewFactory.make(), + bookmarkMainFactory.make(), + myPageMainFactory.make() + ]) + + let navigationController = UINavigationController(rootViewController: tabBar) + navigationController.isNavigationBarHidden = true + setRoot(navigationController) + } + + public func showLogin(exitRoute: LoginExitRoute) { + let loginVC = loginFactory.make(exitRoute: exitRoute) { [weak self] in + switch exitRoute { + case .home: + self?.showMainTab() + default: + break + } + } + + let navigationController = UINavigationController(rootViewController: loginVC) + setRoot(navigationController) + } + + // MARK: - Private Helper + private func setRoot(_ viewController: UIViewController) { + window.rootViewController = viewController + window.makeKeyAndVisible() + } +} diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 0f35d077..bfeb98e4 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -1,3 +1,6 @@ +// swiftlint:disable function_body_length +// swiftlint:disable line_length + import os import UIKit import UserNotifications @@ -96,31 +99,43 @@ private extension AppDelegate { func registerProvider() { DIContainer.register(type: NetworkProvider.self) { - return NetworkProviderImpl() + NetworkProviderImpl() } DIContainer.register(type: SocialAuthenticatableProvider.self, name: "kakao") { - return KakaoLoginProviderImpl() + KakaoLoginProviderImpl() } DIContainer.register(type: SocialAuthenticatableProvider.self, name: "apple") { - return AppleLoginProviderImpl() + AppleLoginProviderImpl() + } + DIContainer.register(type: Interceptor.self) { + TokenInterceptor(fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self)) } } func registerRepository() { DIContainer.register(type: AuthAPIRepository.self) { - return AuthAPIRepositoryImpl( + AuthAPIRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), - interceptor: TokenInterceptor(fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self)) + interceptor: DIContainer.resolve(type: Interceptor.self) ) } DIContainer.register(type: TokenRepository.self) { - return KeyChainRepositoryImpl() - } - DIContainer.register(type: DictionaryListRepository.self) { - return DictionaryListRepositoryImpl(allItems: []) + KeyChainRepositoryImpl() } DIContainer.register(type: DictionaryDetailAPIRepository.self) { - return DictionaryDetailAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), tokenInterceptor: TokenInterceptor(fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self))) + DictionaryDetailAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), tokenInterceptor: DIContainer.resolve(type: Interceptor.self)) + } + DIContainer.register(type: DictionaryListAPIRepository.self) { + DictionaryListAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), tokenInterceptor: DIContainer.resolve(type: Interceptor.self)) + } + DIContainer.register(type: BookmarkRepository.self) { + BookmarkRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), interceptor: DIContainer.resolve(type: Interceptor.self)) + } + DIContainer.register(type: UserDefaultsRepository.self) { + UserDefaultsRepositoryImpl() + } + DIContainer.register(type: AlarmAPIRepository.self) { + AlarmAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), interceptor: DIContainer.resolve(type: Interceptor.self)) } } @@ -134,127 +149,278 @@ private extension AppDelegate { return SocialLoginUseCaseImpl(provider: provider) } DIContainer.register(type: CheckEmptyLevelAndRoleUseCase.self) { - return CheckEmptyLevelAndRoleUseCaseImpl() + CheckEmptyLevelAndRoleUseCaseImpl() } DIContainer.register(type: CheckValidLevelUseCase.self) { - return CheckValidLevelUseCaseImpl() + CheckValidLevelUseCaseImpl() } DIContainer.register(type: FetchJobListUseCase.self) { - return FetchJobListUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + FetchJobListUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: LoginWithAppleUseCase.self) { - return LoginWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + LoginWithAppleUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self), userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self)) } DIContainer.register(type: LoginWithKakaoUseCase.self) { - return LoginWithKakaoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + LoginWithKakaoUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self), userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self)) } DIContainer.register(type: SignUpWithAppleUseCase.self) { - return SignUpWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + SignUpWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: SignUpWithKakaoUseCase.self) { - return SignUpWithKakaoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + SignUpWithKakaoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: UpdateUserInfoUseCase.self) { - return UpdateUserInfoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + UpdateUserInfoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: ReissueUseCase.self) { - return ReissueUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + ReissueUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: PutFCMTokenUseCase.self) { - return PutFCMTokenUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + PutFCMTokenUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: FetchTokenFromLocalUseCase.self) { - return FetchTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) + FetchTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: SaveTokenToLocalUseCase.self) { - return SaveTokenToLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) + SaveTokenToLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: DeleteTokenFromLocalUseCase.self) { - return DeleteTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) + DeleteTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: UpdateMarketingAgreementUseCase.self) { - return UpdateMarketingAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self)) + UpdateMarketingAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: CheckNotificationPermissionUseCase.self) { - return CheckNotificationPermissionUseCaseImpl() + CheckNotificationPermissionUseCaseImpl() } DIContainer.register(type: OpenNotificationSettingUseCase.self) { - return OpenNotificationSettingUseCaseImpl() + OpenNotificationSettingUseCaseImpl() } DIContainer.register(type: UpdateNotificationAgreementUseCase.self) { - return UpdateNotificationAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self)) + UpdateNotificationAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self)) } - DIContainer.register(type: FetchDictionaryItemsUseCase.self) { - return FetchDictionaryItemsUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListRepository.self)) + DIContainer.register(type: CheckLoginUseCase.self) { + CheckLoginUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self)) } - DIContainer.register(type: ToggleBookmarkUseCase.self) { - return ToggleBookmarkUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListRepository.self)) + DIContainer.register(type: FetchDictionaryAllListUseCase.self) { + FetchDictionaryAllListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } - DIContainer.register(type: FetchNotificationUseCase.self) { - return FetchNotificationUseCaseImpl() + DIContainer.register(type: SetBookmarkUseCase.self) { + SetBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) } - DIContainer.register(type: FetchDictionaryDetailMonsterItemsUseCase.self) { - return FetchDictionaryDetailMonsterDropItemUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) - } - DIContainer.register(type: FetchDictionaryDetailMonsterMapUseCase.self) { - return FetchDictionaryDetailMonsterMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + DIContainer.register(type: FetchPlatformUseCase.self) { + FetchPlatformUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) } DIContainer.register(type: FetchDictionaryMapListUseCase.self) { - return FetchDictionaryMapListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryMapListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryItemListUseCase.self) { - return FetchDictionaryItemListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryItemListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryQuestListUseCase.self) { - return FetchDictionaryQuestListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryQuestListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryNpcListUseCase.self) { - return FetchDictionaryNpcListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryNpcListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryMonsterListUseCase.self) { - return FetchDictionaryMonsterListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryMonsterListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailMonsterUseCase.self) { + FetchDictionaryDetailMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailMonsterItemsUseCase.self) { + FetchDictionaryDetailMonsterDropItemUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailMonsterMapUseCase.self) { + FetchDictionaryDetailMonsterMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailNpcUseCase.self) { + FetchDictionaryDetailNpcUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailNpcQuestUseCase.self) { + FetchDictionaryDetailNpcQuestUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailNpcMapUseCase.self) { + FetchDictionaryDetailNpcMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailItemUseCase.self) { + FetchDictionaryDetailItemUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailItemDropMonsterUseCase.self) { + FetchDictionaryDetailItemDropMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailQuestUseCase.self) { + FetchDictionaryDetailQuestUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailQuestLinkedQuestsUseCase.self) { + FetchDictionaryDetailQuestLinkedQuestsUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailMapUseCase.self) { + FetchDictionaryDetailMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailMapSpawnMonsterUseCase.self) { + FetchDictionaryDetailMapSpawnMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: FetchDictionaryDetailMapNpcUseCase.self) { + FetchDictionaryDetailMapNpcUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + } + DIContainer.register(type: RecentSearchRemoveUseCase.self) { + RecentSearchRemoveUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } + DIContainer.register(type: RecentSearchAddUseCase.self) { + RecentSearchAddUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } + DIContainer.register(type: FetchDictionaryListCountUseCase.self) { + FetchDictionaryListCountUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + } + DIContainer.register(type: FetchDictionarySearchListUseCase.self) { + FetchDictionarySearchListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + } + DIContainer.register(type: RecentSearchFetchUseCase.self) { + RecentSearchFetchUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } + DIContainer.register(type: FetchBookmarkUseCase.self) { + FetchBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) + } + DIContainer.register(type: FetchMonsterBookmarkUseCase.self) { + FetchMonsterBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) + } + DIContainer.register(type: FetchItemBookmarkUseCase.self) { + FetchItemBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) + } + DIContainer.register(type: FetchNPCBookmarkUseCase.self) { + FetchNPCBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) + } + DIContainer.register(type: FetchQuestBookmarkUseCase.self) { + FetchQuestBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) + } + DIContainer.register(type: FetchMapBookmarkUseCase.self) { + FetchMapBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) + } + DIContainer.register(type: UpdateProfileImageUseCase.self) { + UpdateProfileImageUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + } + DIContainer.register(type: FetchJobUseCase.self) { + FetchJobUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + } + DIContainer.register(type: FetchProfileUseCase.self) { + FetchProfileUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self), fetchJobUseCase: DIContainer.resolve(type: FetchJobUseCase.self)) + } + DIContainer.register(type: CheckNickNameUseCase.self) { + CheckNickNameUseCaseImpl() + } + DIContainer.register(type: UpdateNickNameUseCase.self) { + UpdateNickNameUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + } + DIContainer.register(type: LogoutUseCase.self) { + LogoutUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) + } + DIContainer.register(type: WithdrawUseCase.self) { + WithdrawUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self)) + } + DIContainer.register(type: FetchNoticesUseCase.self) { + FetchNoticesUseCaseImpl(repository: DIContainer.resolve(type: AlarmAPIRepository.self)) + } + DIContainer.register(type: FetchOngoingEventsUseCase.self) { + FetchOngoingEventsUseCaseImpl(repository: DIContainer.resolve(type: AlarmAPIRepository.self)) + } + DIContainer.register(type: FetchOutdatedEventsUseCase.self) { + FetchOutdatedEventsUseCaseImpl(repository: DIContainer.resolve(type: AlarmAPIRepository.self)) + } + DIContainer.register(type: FetchPatchNotesUseCase.self) { + FetchPatchNotesUseCaseImpl(repository: DIContainer.resolve(type: AlarmAPIRepository.self)) + } + DIContainer.register(type: SetReadUseCase.self) { + SetReadUseCaseImpl(repository: DIContainer.resolve(type: AlarmAPIRepository.self)) + } + DIContainer.register(type: FetchAllAlarmUseCase.self) { + FetchAllAlarmUseCaseImpl(repository: DIContainer.resolve(type: AlarmAPIRepository.self)) + } + DIContainer.register(type: ParseItemFilterResultUseCase.self) { + ParseItemFilterResultUseCaseImpl() } } func registerFactory() { DIContainer.register(type: ItemFilterBottomSheetFactory.self) { - return ItemFilterBottomSheetFactoryImpl() + ItemFilterBottomSheetFactoryImpl() } DIContainer.register(type: MonsterFilterBottomSheetFactory.self) { - return MonsterFilterBottomSheetFactoryImpl() + MonsterFilterBottomSheetFactoryImpl() } DIContainer.register(type: SortedBottomSheetFactory.self) { - return SortedBottomSheetFactoryImpl() + SortedBottomSheetFactoryImpl() } DIContainer.register(type: AddCollectionFactory.self) { - return AddCollectionFactoryImpl() + AddCollectionFactoryImpl() } DIContainer.register(type: BookmarkModalFactory.self) { - return BookmarkModalFactoryImpl(addCollectionFactory: DIContainer.resolve(type: AddCollectionFactory.self)) + BookmarkModalFactoryImpl(addCollectionFactory: DIContainer.resolve(type: AddCollectionFactory.self)) + } + DIContainer.register(type: LoginFactory.self) { + LoginFactoryImpl( + termsAgreementsFactory: DIContainer.resolve(type: TermsAgreementFactory.self), + appleLoginUseCase: DIContainer.resolve(type: FetchSocialCredentialUseCase.self, name: "apple"), + kakaoLoginUseCase: DIContainer.resolve(type: FetchSocialCredentialUseCase.self, name: "kakao"), + loginWithAppleUseCase: DIContainer.resolve(type: LoginWithAppleUseCase.self), + loginWithKakaoUseCase: DIContainer.resolve(type: LoginWithKakaoUseCase.self), + fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self), + putFCMTokenUseCase: DIContainer.resolve(type: PutFCMTokenUseCase.self), fetchPlatformUseCase: DIContainer.resolve(type: FetchPlatformUseCase.self) + ) } DIContainer.register(type: DictionaryDetailFactory.self) { - return DictionaryDetailFactoryImpl(dictionaryDetailMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterUseCase.self), dictionaryDetailMonsterDropItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterItemsUseCase.self), dictionaryDetailMonsterMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterMapUseCase.self)) + DictionaryDetailFactoryImpl(loginFactory: { DIContainer.resolve(type: LoginFactory.self) }, bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), dictionaryDetailMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapUseCase.self), dictionaryDetailMapSpawnMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapSpawnMonsterUseCase.self), dictionaryDetailMapNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapNpcUseCase.self), dictionaryDetailQuestLinkedQuestsUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestLinkedQuestsUseCase.self), dictionaryDetailQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestUseCase.self), dictionaryDetailItemDropMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemDropMonsterUseCase.self), dictionaryDetailItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemUseCase.self), dictionaryDetailNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcUseCase.self), dictionaryDetailNpcQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcQuestUseCase.self), dictionaryDetailNpcMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcMapUseCase.self), dictionaryDetailMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterUseCase.self), dictionaryDetailMonsterDropItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterItemsUseCase.self), dictionaryDetailMonsterMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterMapUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self)) } DIContainer.register(type: DictionaryMainListFactory.self) { - return DictionaryListFactoryImpl(dictionaryMapListItemUseCase: DIContainer.resolve(type: FetchDictionaryMapListUseCase.self), dictionaryItemListItemUseCase: DIContainer.resolve(type: FetchDictionaryItemListUseCase.self), dictionaryQuestListItemUseCase: DIContainer.resolve(type: FetchDictionaryQuestListUseCase.self), dictionaryNpcListItemUseCase: DIContainer.resolve(type: FetchDictionaryNpcListUseCase.self), dictionaryListItemUseCase: DIContainer.resolve(type: FetchDictionaryMonsterListUseCase.self), fetchDictionaryItemsUseCase: DIContainer.resolve(type: FetchDictionaryItemsUseCase.self), toggleBookmarkUseCase: DIContainer.resolve(type: ToggleBookmarkUseCase.self), itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), detailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self)) + DictionaryListFactoryImpl( + checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), + dictionaryAllListItemUseCase: DIContainer.resolve(type: FetchDictionaryAllListUseCase.self), + dictionaryMapListItemUseCase: DIContainer.resolve(type: FetchDictionaryMapListUseCase.self), + dictionaryItemListItemUseCase: DIContainer.resolve(type: FetchDictionaryItemListUseCase.self), + dictionaryQuestListItemUseCase: DIContainer.resolve(type: FetchDictionaryQuestListUseCase.self), + dictionaryNpcListItemUseCase: DIContainer + .resolve(type: FetchDictionaryNpcListUseCase.self), + dictionaryListItemUseCase: DIContainer.resolve(type: FetchDictionaryMonsterListUseCase.self), + setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), parseItemFilterResultUseCase: DIContainer.resolve(type: ParseItemFilterResultUseCase.self), + itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), + monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), + sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), + bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), + detailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self), loginFactory: { DIContainer.resolve(type: LoginFactory.self) } + ) } DIContainer.register(type: DictionarySearchResultFactory.self) { - return DictionarySearchResultFactoryImpl(dictionaryMainListFactory: DIContainer.resolve(type: DictionaryMainListFactory.self)) + DictionarySearchResultFactoryImpl( + dictionaryListCountUseCase: DIContainer.resolve(type: FetchDictionaryListCountUseCase.self), + dictionaryMainListFactory: DIContainer + .resolve(type: DictionaryMainListFactory.self), + dictionarySearchListUseCase: DIContainer.resolve(type: FetchDictionarySearchListUseCase.self) + ) } DIContainer.register(type: DictionarySearchFactory.self) { - return DictionarySearchFactoryImpl(searchResultFactory: DIContainer.resolve(type: DictionarySearchResultFactory.self)) + DictionarySearchFactoryImpl(recentSearchRemoveUseCase: DIContainer.resolve(type: RecentSearchRemoveUseCase.self), + recentSearchAddUseCase: DIContainer.resolve(type: RecentSearchAddUseCase.self), + searchResultFactory: DIContainer + .resolve(type: DictionarySearchResultFactory.self), recentSearchFetchUseCase: DIContainer.resolve(type: RecentSearchFetchUseCase.self)) } DIContainer.register(type: NotificationSettingFactory.self) { - return NotificationSettingFactoryImpl(checkNotificationPermissionUseCase: DIContainer.resolve(type: CheckNotificationPermissionUseCase.self), updateNotificationAgreementUseCase: DIContainer.resolve(type: UpdateNotificationAgreementUseCase.self)) + NotificationSettingFactoryImpl(checkNotificationPermissionUseCase: DIContainer.resolve(type: CheckNotificationPermissionUseCase.self), updateNotificationAgreementUseCase: DIContainer.resolve(type: UpdateNotificationAgreementUseCase.self)) } DIContainer.register(type: DictionaryNotificationFactory.self) { - return DictionaryNotificationFactoryImpl(fetchNotificationUseCase: DIContainer.resolve(type: FetchNotificationUseCase.self), notificationSettingFactory: DIContainer.resolve(type: NotificationSettingFactory.self)) + DictionaryNotificationFactoryImpl(notificationSettingFactory: DIContainer.resolve(type: NotificationSettingFactory.self), fetchAllAlarmUseCase: DIContainer.resolve(type: FetchAllAlarmUseCase.self), fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self)) } DIContainer.register(type: DictionaryMainViewFactory.self) { - return DictionaryMainViewFactoryImpl(dictionaryMainListFactory: DIContainer.resolve(type: DictionaryMainListFactory.self), searchFactory: DIContainer.resolve(type: DictionarySearchFactory.self), notificationFactory: DIContainer.resolve(type: DictionaryNotificationFactory.self)) + DictionaryMainViewFactoryImpl( + dictionaryMainListFactory: DIContainer + .resolve(type: DictionaryMainListFactory.self), + searchFactory: DIContainer.resolve(type: DictionarySearchFactory.self), + notificationFactory: DIContainer + .resolve(type: DictionaryNotificationFactory.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self) + ) } DIContainer.register(type: OnBoardingNotificationSheetFactory.self) { - return OnBoardingNotificationSheetFactoryImpl( + OnBoardingNotificationSheetFactoryImpl( checkNotificationPermissionUseCase: DIContainer .resolve(type: CheckNotificationPermissionUseCase.self), openNotificationSettingUseCase: DIContainer @@ -264,19 +430,20 @@ private extension AppDelegate { ) } DIContainer.register(type: OnBoardingInputFactory.self) { - return OnBoardingInputFactoryImpl( + OnBoardingInputFactoryImpl( checkEmptyUseCase: DIContainer.resolve(type: CheckEmptyLevelAndRoleUseCase.self), checkValidLevelUseCase: DIContainer.resolve(type: CheckValidLevelUseCase.self), fetchJobListUseCase: DIContainer.resolve(type: FetchJobListUseCase.self), - updateUserInfoUseCase: DIContainer.resolve(type: UpdateUserInfoUseCase.self), onBoardingNotificationFactory: DIContainer.resolve(type: OnBoardingNotificationFactory.self)) + updateUserInfoUseCase: DIContainer.resolve(type: UpdateUserInfoUseCase.self), onBoardingNotificationFactory: DIContainer.resolve(type: OnBoardingNotificationFactory.self) + ) } DIContainer.register(type: OnBoardingQuestionFactory.self) { - return OnBoardingQuestionFactoryImpl( + OnBoardingQuestionFactoryImpl( onBoardingInputFactory: DIContainer.resolve(type: OnBoardingInputFactory.self) ) } DIContainer.register(type: TermsAgreementFactory.self) { - return TermsAgreementFactoryImpl( + TermsAgreementFactoryImpl( onBoardingQuestionFactory: DIContainer.resolve(type: OnBoardingQuestionFactory.self), signUpWithKakaoUseCase: DIContainer.resolve(type: SignUpWithKakaoUseCase.self), signUpWithAppleUseCase: DIContainer.resolve(type: SignUpWithAppleUseCase.self), @@ -284,19 +451,89 @@ private extension AppDelegate { fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self), updateMarketingAgreementUseCase: DIContainer.resolve(type: UpdateMarketingAgreementUseCase.self) ) } - DIContainer.register(type: LoginFactory.self) { - return LoginFactoryImpl( - termsAgreementsFactory: DIContainer.resolve(type: TermsAgreementFactory.self), - appleLoginUseCase: DIContainer.resolve(type: FetchSocialCredentialUseCase.self, name: "apple"), - kakaoLoginUseCase: DIContainer.resolve(type: FetchSocialCredentialUseCase.self, name: "kakao"), - loginWithAppleUseCase: DIContainer.resolve(type: LoginWithAppleUseCase.self), - loginWithKakaoUseCase: DIContainer.resolve(type: LoginWithKakaoUseCase.self), - fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self), - putFCMTokenUseCase: DIContainer.resolve(type: PutFCMTokenUseCase.self) + DIContainer.register(type: OnBoardingNotificationFactory.self) { + OnBoardingNotificationFactoryImpl(onBoardingNotificationSheetFactory: DIContainer.resolve(type: OnBoardingNotificationSheetFactory.self)) + } + DIContainer.register(type: BookmarkMainFactory.self) { + BookmarkMainFactoryImpl( + setBookmarkUseCase: DIContainer + .resolve(type: SetBookmarkUseCase.self), + onBoardingFactory: DIContainer + .resolve(type: BookmarkOnBoardingFactory.self), + bookmarkListFactory: DIContainer + .resolve(type: BookmarkListFactory.self), + collectionListFactory: DIContainer + .resolve(type: CollectionListFactory.self), + searchFactory: DIContainer + .resolve(type: DictionarySearchFactory.self), + notificationFactory: DIContainer.resolve( + type: DictionaryNotificationFactory.self + ) ) } - DIContainer.register(type: OnBoardingNotificationFactory.self) { - return OnBoardingNotificationFactoryImpl(onBoardingNotificationSheetFactory: DIContainer.resolve(type: OnBoardingNotificationSheetFactory.self)) + DIContainer.register(type: BookmarkOnBoardingFactory.self) { + BookmarkOnBoardingFactoryImpl() + } + DIContainer.register(type: BookmarkListFactory.self) { + BookmarkListFactoryImpl(itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), loginFactory: DIContainer.resolve(type: LoginFactory.self), dictionaryDetailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), fetchBookmarkUseCase: DIContainer.resolve(type: FetchBookmarkUseCase.self), fetchMonsterBookmarkUseCase: DIContainer.resolve(type: FetchMonsterBookmarkUseCase.self), fetchItemBookmarkUseCase: DIContainer.resolve(type: FetchItemBookmarkUseCase.self), fetchNPCBookmarkUseCase: DIContainer.resolve(type: FetchNPCBookmarkUseCase.self), fetchQuestBookmarkUseCase: DIContainer.resolve(type: FetchQuestBookmarkUseCase.self), fetchMapBookmarkUseCase: DIContainer.resolve(type: FetchMapBookmarkUseCase.self), collectionEditFactory: DIContainer.resolve(type: CollectionEditFactory.self)) + } + DIContainer.register(type: CollectionListFactory.self) { + CollectionListFactoryImpl(addCollectionFactory: DIContainer.resolve(type: AddCollectionFactory.self), bookmarkDetailFactory: DIContainer.resolve(type: CollectionDetailFactory.self)) + } + DIContainer.register(type: CollectionDetailFactory.self) { + CollectionDetailFactoryImpl( + setBookmarkUseCase: DIContainer + .resolve(type: SetBookmarkUseCase.self), + bookmarkModalFactory: DIContainer + .resolve(type: BookmarkModalFactory.self), + collectionSettingFactory: DIContainer + .resolve(type: CollectionSettingFactory.self), + addCollectionFactory: DIContainer + .resolve(type: AddCollectionFactory.self), + collectionEditFactory: DIContainer + .resolve(type: CollectionEditFactory.self), + dictionaryDetailFactory: DIContainer + .resolve(type: DictionaryDetailFactory.self) + ) + } + DIContainer.register(type: CollectionSettingFactory.self) { + CollectionSettingFactoryImpl() + } + DIContainer.register(type: CollectionEditFactory.self) { + CollectionEditFactoryImpl(setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self)) + } + DIContainer.register(type: MyPageMainFactory.self) { + MyPageMainFactoryImpl( + loginFactory: DIContainer.resolve(type: LoginFactory.self), setProfileFactory: DIContainer + .resolve(type: SetProfileFactory.self), + customerSupportFactory: DIContainer + .resolve(type: CustomerSupportFactory.self), + notificationSettingFactory: DIContainer + .resolve(type: NotificationSettingFactory.self), + setCharacterFactory: DIContainer + .resolve(type: SetCharacterFactory.self), fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self) + ) + } + DIContainer.register(type: CustomerSupportFactory.self) { + CustomerSupportBaseViewFactoryImpl(fetchNoticesUseCase: DIContainer.resolve(type: FetchNoticesUseCase.self), fetchOngoingEventsUseCase: DIContainer.resolve(type: FetchOngoingEventsUseCase.self), fetchOutdatedEventsUseCase: DIContainer.resolve(type: FetchOutdatedEventsUseCase.self), fetchPatchNotesUseCase: DIContainer.resolve(type: FetchPatchNotesUseCase.self), setReadUseCase: DIContainer.resolve(type: SetReadUseCase.self)) + } + DIContainer.register(type: SetProfileFactory.self) { + SetProfileFactoryImpl(selectImageFactory: DIContainer.resolve(type: SelectImageFactory.self), checkNickNameUseCase: DIContainer.resolve(type: CheckNickNameUseCase.self), updateNickNameUseCase: DIContainer.resolve(type: UpdateNickNameUseCase.self), logoutUseCase: DIContainer.resolve(type: LogoutUseCase.self), withdrawUseCase: DIContainer.resolve(type: WithdrawUseCase.self), fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self)) + } + DIContainer.register(type: SetCharacterFactory.self) { + SetCharacterFactoryImpl( + checkEmptyUseCase: DIContainer + .resolve(type: CheckEmptyLevelAndRoleUseCase.self), + checkValidLevelUseCase: DIContainer + .resolve(type: CheckValidLevelUseCase.self), + fetchJobListUseCase: DIContainer + .resolve(type: FetchJobListUseCase.self), + updateUserInfoUseCase: DIContainer + .resolve(type: UpdateUserInfoUseCase.self) + ) + } + DIContainer.register(type: SelectImageFactory.self) { + SelectImageFactoryImpl(updateProfileImageUseCase: DIContainer.resolve(type: UpdateProfileImageUseCase.self)) } } } diff --git a/MLS/MLS/Application/SceneDelegate.swift b/MLS/MLS/Application/SceneDelegate.swift index 1b0e1044..979f6eb8 100644 --- a/MLS/MLS/Application/SceneDelegate.swift +++ b/MLS/MLS/Application/SceneDelegate.swift @@ -1,82 +1,76 @@ -import NotificationCenter import UIKit -import AuthFeature import AuthFeatureInterface -import BaseFeature +import BookmarkFeatureInterface import Core -import Data -import DictionaryFeature import DictionaryFeatureInterface -import Domain import DomainInterface +import MyPageFeatureInterface -import KakaoSDKAuth import RxSwift -class SceneDelegate: UIResponder, UIWindowSceneDelegate { +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + var appCoordinator: AppCoordinator? var disposeBag = DisposeBag() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } - window = UIWindow(windowScene: windowScene) - setStartViewController(window: window) - } - func sceneDidDisconnect(_ scene: UIScene) {} + let window = UIWindow(windowScene: windowScene) + window.makeKeyAndVisible() + self.window = window - func sceneDidBecomeActive(_ scene: UIScene) {} + let dictionaryMainViewFactory: DictionaryMainViewFactory = DIContainer.resolve(type: DictionaryMainViewFactory.self) + let bookmarkMainFactory: BookmarkMainFactory = DIContainer.resolve(type: BookmarkMainFactory.self) + let myPageMainFactory: MyPageMainFactory = DIContainer.resolve(type: MyPageMainFactory.self) + let loginFactory: LoginFactory = DIContainer.resolve(type: LoginFactory.self) - func sceneWillResignActive(_ scene: UIScene) {} + let coordinator = AppCoordinator( + window: window, + dictionaryMainViewFactory: dictionaryMainViewFactory, + bookmarkMainFactory: bookmarkMainFactory, + myPageMainFactory: myPageMainFactory, + loginFactory: loginFactory + ) + self.appCoordinator = coordinator - func sceneWillEnterForeground(_ scene: UIScene) {} + startScene(coordinator: coordinator) + } - func sceneDidEnterBackground(_ scene: UIScene) {} + private func startScene(coordinator: AppCoordinator) { + let fetchTokenUseCase = DIContainer.resolve(type: FetchTokenFromLocalUseCase.self) + let reissueUseCase = DIContainer.resolve(type: ReissueUseCase.self) + let saveTokenUseCase = DIContainer.resolve(type: SaveTokenToLocalUseCase.self) - func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { - if let url = URLContexts.first?.url { - if AuthApi.isKakaoTalkLoginUrl(url) { - _ = AuthController.handleOpenUrl(url: url) - } - } - } + let fetchResult = fetchTokenUseCase.execute(type: .refreshToken) - func setStartViewController(window: UIWindow?) { - UNUserNotificationCenter.current().getNotificationSettings { [weak self] _ in - guard let self = self else { return } - DispatchQueue.main.async { - let loginFactory: LoginFactory = DIContainer.resolve(type: LoginFactory.self) - let notificationFactory: OnBoardingNotificationFactory = DIContainer.resolve(type: OnBoardingNotificationFactory.self) - window?.makeKeyAndVisible() - let reissueUseCase = DIContainer.resolve(type: ReissueUseCase.self) - let fetchTokenUseCase = DIContainer.resolve(type: FetchTokenFromLocalUseCase.self) - let saveTokenUseCase = DIContainer.resolve(type: SaveTokenToLocalUseCase.self) - let fetchResult = fetchTokenUseCase.execute(type: .refreshToken) + switch fetchResult { + case .success(let refreshToken): + // ✅ refreshToken 존재 → accessToken 재발급 시도 + reissueUseCase.execute(refreshToken: refreshToken) + .observe(on: MainScheduler.instance) + .subscribe( + onNext: { response in + let accessSave = saveTokenUseCase.execute(type: .accessToken, value: response.accessToken) + let refreshSave = saveTokenUseCase.execute(type: .refreshToken, value: response.refreshToken) - switch fetchResult { - case .success(let token): - reissueUseCase.execute(refreshToken: token) - .observe(on: MainScheduler.instance) - .subscribe { response in - let accessSaveResult = saveTokenUseCase.execute(type: .accessToken, value: response.accessToken) - let refreshSaveResult = saveTokenUseCase.execute(type: .refreshToken, value: response.refreshToken) - window?.rootViewController = UINavigationController(rootViewController: ViewController()) - if case .success = accessSaveResult, case .success = refreshSaveResult { - // 저장 결과 모두 성공일 때만 진입 - window?.rootViewController = UINavigationController(rootViewController: ViewController()) - } else { - // 저장 실패 시 로그인 화면으로 이동 - window?.rootViewController = UINavigationController(rootViewController: loginFactory.make(isReLogin: false)) - } - } onError: { _ in - window?.rootViewController = UINavigationController(rootViewController: loginFactory.make(isReLogin: false)) + if case .success = accessSave, case .success = refreshSave { + coordinator.showMainTab() + } else { + coordinator.showLogin(exitRoute: .home) } - .disposed(by: self.disposeBag) - case .failure: - window?.rootViewController = UINavigationController(rootViewController: loginFactory.make(isReLogin: false)) - } - } + }, + onError: { error in + print(error) + coordinator.showLogin(exitRoute: .home) + } + ) + .disposed(by: disposeBag) + + case .failure: + // ✅ refreshToken 없으면 바로 로그인으로 + coordinator.showLogin(exitRoute: .home) } } } diff --git a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginFactoryImpl.swift b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginFactoryImpl.swift index a4f8f9ee..a75289b0 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginFactoryImpl.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginFactoryImpl.swift @@ -2,6 +2,8 @@ import AuthFeatureInterface import BaseFeature import DomainInterface +import RxSwift + public struct LoginFactoryImpl: LoginFactory { private let termsAgreementsFactory: TermsAgreementFactory private let appleLoginUseCase: FetchSocialCredentialUseCase @@ -10,6 +12,7 @@ public struct LoginFactoryImpl: LoginFactory { private let loginWithKakaoUseCase: LoginWithKakaoUseCase private let fetchTokenUseCase: FetchTokenFromLocalUseCase private let putFCMTokenUseCase: PutFCMTokenUseCase + private let fetchPlatformUseCase: FetchPlatformUseCase public init( termsAgreementsFactory: TermsAgreementFactory, @@ -18,7 +21,8 @@ public struct LoginFactoryImpl: LoginFactory { loginWithAppleUseCase: LoginWithAppleUseCase, loginWithKakaoUseCase: LoginWithKakaoUseCase, fetchTokenUseCase: FetchTokenFromLocalUseCase, - putFCMTokenUseCase: PutFCMTokenUseCase + putFCMTokenUseCase: PutFCMTokenUseCase, + fetchPlatformUseCase: FetchPlatformUseCase ) { self.termsAgreementsFactory = termsAgreementsFactory self.appleLoginUseCase = appleLoginUseCase @@ -27,20 +31,35 @@ public struct LoginFactoryImpl: LoginFactory { self.loginWithKakaoUseCase = loginWithKakaoUseCase self.fetchTokenUseCase = fetchTokenUseCase self.putFCMTokenUseCase = putFCMTokenUseCase + self.fetchPlatformUseCase = fetchPlatformUseCase } - public func make( - isReLogin: Bool - ) -> BaseViewController { - let viewController = LoginViewController(isRelogin: isReLogin, termsAgreementsFactory: termsAgreementsFactory) + public func make(exitRoute: LoginExitRoute, onLoginCompleted: (() -> Void)?) -> BaseViewController { + let viewController = LoginViewController(termsAgreementsFactory: termsAgreementsFactory) + viewController.isBottomTabbarHidden = true + viewController.reactor = LoginReactor( fetchAppleCredentialUseCase: appleLoginUseCase, fetchKakaoCredentialUseCase: kakaoLoginUseCase, loginWithAppleUseCase: loginWithAppleUseCase, loginWithKakaoUseCase: loginWithKakaoUseCase, fetchTokenUseCase: fetchTokenUseCase, - putFCMTokenUseCase: putFCMTokenUseCase + putFCMTokenUseCase: putFCMTokenUseCase, + fetchPlatformUseCase: fetchPlatformUseCase ) + + viewController.routeToHome + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak viewController] in + switch exitRoute { + case .home: + onLoginCompleted?() + case .pop: + viewController?.navigationController?.popViewController(animated: true) + } + }) + .disposed(by: viewController.disposeBag) + return viewController } } diff --git a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginReactor.swift b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginReactor.swift index 6989a7ee..5aa15ac3 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginReactor.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginReactor.swift @@ -8,12 +8,13 @@ public final class LoginReactor: Reactor { public enum Route { case none case termsAgreements(credential: Credential, platform: LoginPlatform) - case home case error + case dismiss } // MARK: - Reactor public enum Action { + case viewWillAppear case kakaoLoginButtonTapped case appleLoginButtonTapped case guestLoginButtonTapped @@ -21,10 +22,12 @@ public final class LoginReactor: Reactor { public enum Mutation { case navigateTo(route: Route) + case setRelogin(LoginPlatform?) } public struct State { @Pulse var route: Route = .none + var platform: LoginPlatform? } // MARK: - properties @@ -36,6 +39,7 @@ public final class LoginReactor: Reactor { private let loginWithKakaoUseCase: LoginWithKakaoUseCase private let fetchTokenUseCase: FetchTokenFromLocalUseCase private let putFCMTokenUseCase: PutFCMTokenUseCase + private let fetchPlatformUseCase: FetchPlatformUseCase // MARK: - init public init( @@ -44,7 +48,8 @@ public final class LoginReactor: Reactor { loginWithAppleUseCase: LoginWithAppleUseCase, loginWithKakaoUseCase: LoginWithKakaoUseCase, fetchTokenUseCase: FetchTokenFromLocalUseCase, - putFCMTokenUseCase: PutFCMTokenUseCase + putFCMTokenUseCase: PutFCMTokenUseCase, + fetchPlatformUseCase: FetchPlatformUseCase ) { self.fetchAppleCredentialUseCase = fetchAppleCredentialUseCase self.fetchKakaoCredentialUseCase = fetchKakaoCredentialUseCase @@ -52,18 +57,22 @@ public final class LoginReactor: Reactor { self.loginWithKakaoUseCase = loginWithKakaoUseCase self.fetchTokenUseCase = fetchTokenUseCase self.putFCMTokenUseCase = putFCMTokenUseCase + self.fetchPlatformUseCase = fetchPlatformUseCase self.initialState = State() } // MARK: - Reactor Methods public func mutate(action: Action) -> Observable { switch action { + case .viewWillAppear: + return fetchPlatformUseCase.execute() + .map { Mutation.setRelogin($0) } case .kakaoLoginButtonTapped: return handleKakaoLogin() case .appleLoginButtonTapped: return handleAppleLogin() case .guestLoginButtonTapped: - return .just(.navigateTo(route: .home)) + return .just(.navigateTo(route: .dismiss)) } } @@ -72,6 +81,8 @@ public final class LoginReactor: Reactor { switch mutation { case .navigateTo(let route): newState.route = route + case .setRelogin(let platform): + newState.platform = platform } return newState } @@ -92,7 +103,7 @@ private extension LoginReactor { if response.isRegister { // 3. 회원가입된 유저면 FCM 토큰 등록 후 홈으로 이동 return owner.putFCMTokenUseCase.execute(credential: response.accessToken, fcmToken: fcmToken) - .andThen(.just(.navigateTo(route: .home))) + .andThen(.just(.navigateTo(route: .dismiss))) } else { // 4. 미가입 유저면 약관 동의 화면으로 이동 return .just(.navigateTo(route: .termsAgreements( @@ -129,7 +140,7 @@ private extension LoginReactor { if response.isRegister { // 3. 회원가입된 유저면 FCM 토큰 등록 후 홈으로 이동 return owner.putFCMTokenUseCase.execute(credential: response.accessToken, fcmToken: fcmToken) - .andThen(.just(.navigateTo(route: .home))) + .andThen(.just(.navigateTo(route: .dismiss))) } else { // 4. 미가입 유저면 약관 동의 화면으로 이동 return .just(.navigateTo(route: .termsAgreements( diff --git a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginView.swift b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginView.swift index 60b6ee9f..51437863 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginView.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginView.swift @@ -1,6 +1,7 @@ import UIKit import DesignSystem +import DomainInterface import SnapKit @@ -78,8 +79,6 @@ final class LoginView: UIView { return button }() - private let isRelogin: Bool - private let mainTitleLabel: UILabel = { let label = UILabel() label.attributedText = .makeStyledString(font: .h_xl_b, text: "모험가님,") @@ -93,8 +92,7 @@ final class LoginView: UIView { }() // MARK: - init - init(isRelogin: Bool) { - self.isRelogin = isRelogin + init() { super.init(frame: .zero) addViews() @@ -113,16 +111,10 @@ private extension LoginView { func addViews() { addSubview(loginImageView) addSubview(buttonStackView) + buttonStackView.addArrangedSubview(kakaoLoginButton) buttonStackView.addArrangedSubview(appleLoginButton) - if isRelogin { - addSubview(mainTitleLabel) - addSubview(subTitleLabel) - } else { - buttonStackView.addArrangedSubview(guestLoginButton) - } - kakaoLoginButton.addSubview(kakaoLogoImageView) kakaoLoginButton.addSubview(kakaoLoginLabel) appleLoginButton.addSubview(appleLogoImageView) @@ -170,24 +162,42 @@ private extension LoginView { make.centerY.equalToSuperview() make.centerX.equalToSuperview().inset(Constant.buttonCenterXInset) } + } + + func configureUI() {} +} + +extension LoginView { + func update(loginPlatform: LoginPlatform?) { + mainTitleLabel.removeFromSuperview() + subTitleLabel.removeFromSuperview() + guestLoginButton.removeFromSuperview() + + switch loginPlatform { + case .kakao, .apple: + // 최근로그인 라벨 추가 + addSubview(mainTitleLabel) + addSubview(subTitleLabel) - if isRelogin { - subTitleLabel.snp.makeConstraints { make in + subTitleLabel.snp.remakeConstraints { make in make.bottom.equalTo(buttonStackView.snp.top).offset(Constant.subTitleBottomSpacing) make.centerX.equalToSuperview() make.height.equalTo(Constant.labelHeight) } - mainTitleLabel.snp.makeConstraints { make in + mainTitleLabel.snp.remakeConstraints { make in make.bottom.equalTo(subTitleLabel.snp.top) make.centerX.equalToSuperview() make.height.equalTo(Constant.labelHeight) } - } else { - guestLoginButton.snp.makeConstraints { make in + case nil: + buttonStackView.addArrangedSubview(guestLoginButton) + guestLoginButton.snp.remakeConstraints { make in make.height.equalTo(Constant.buttonHeight) } + } - } - func configureUI() {} + setNeedsLayout() + layoutIfNeeded() + } } diff --git a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginViewController.swift index 4cbbe924..5a1ba53e 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/Login/LoginViewController.swift @@ -14,12 +14,14 @@ public final class LoginViewController: BaseViewController, View { // MARK: - Properties public var disposeBag = DisposeBag() + public let routeToHome = PublishRelay() + private let mainView: LoginView private let termsAgreementsFactory: TermsAgreementFactory - public init(isRelogin: Bool, termsAgreementsFactory: TermsAgreementFactory) { - self.mainView = LoginView(isRelogin: isRelogin) + public init(termsAgreementsFactory: TermsAgreementFactory) { + self.mainView = LoginView() self.termsAgreementsFactory = termsAgreementsFactory super.init() } @@ -66,6 +68,11 @@ public extension LoginViewController { } func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + mainView.kakaoLoginButton.rx.tap .map { Reactor.Action.kakaoLoginButtonTapped } .bind(to: reactor.action) @@ -111,6 +118,16 @@ public extension LoginViewController { } func bindViewState(reactor: Reactor) { + reactor.state + .observe(on: MainScheduler.instance) + .map { $0.platform } + .distinctUntilChanged() + .withUnretained(self) + .subscribe { owner, platform in + owner.mainView.update(loginPlatform: platform) + } + .disposed(by: disposeBag) + rx.viewDidAppear .take(1) .flatMapLatest { _ in reactor.pulse(\.$route) } @@ -121,10 +138,8 @@ public extension LoginViewController { case .termsAgreements(let credential, let platform): let controller = owner.termsAgreementsFactory.make(credential: credential, platform: platform) owner.navigationController?.pushViewController(controller, animated: true) - case .home: - let controller = UIViewController() - controller.view.backgroundColor = .green - owner.navigationController?.pushViewController(controller, animated: true) + case .dismiss: + owner.routeToHome.accept(()) case .error: DispatchQueue.main.async { let controller = BaseErrorViewController() diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift index ba6d82dd..4606d009 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift @@ -13,10 +13,11 @@ public class OnBoardingNotificationViewController: BaseViewController, View { // MARK: - Properties public typealias Reactor = OnBoardingNotificationReactor + public var disposeBag = DisposeBag() + private let onBoardingNotificationSheetFactory: OnBoardingNotificationSheetFactory // MARK: - Components - public var disposeBag = DisposeBag() private var mainView = OnBoardingNotificationView() diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingInput/OnBoardingInputViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingInput/OnBoardingInputViewController.swift index 55a13222..93ec8eb6 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingInput/OnBoardingInputViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingInput/OnBoardingInputViewController.swift @@ -14,10 +14,11 @@ public class OnBoardingInputViewController: BaseViewController, View { // MARK: - Properties public typealias Reactor = OnBoardingInputReactor + public var disposeBag = DisposeBag() + private let onBoardingNotificationFactory: OnBoardingNotificationFactory // MARK: - Components - public var disposeBag = DisposeBag() private var mainView = OnBoardingInputView() diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetFactoryImpl.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetFactoryImpl.swift index 1f19b9a3..85de382f 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetFactoryImpl.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetFactoryImpl.swift @@ -10,7 +10,13 @@ public struct OnBoardingNotificationSheetFactoryImpl: OnBoardingNotificationShee private let updateUserInfoUseCase: UpdateUserInfoUseCase private let dictionaryMainViewFactory: DictionaryMainViewFactory - public init(checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase, openNotificationSettingUseCase: OpenNotificationSettingUseCase, updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase, updateUserInfoUseCase: UpdateUserInfoUseCase, dictionaryMainViewFactory: DictionaryMainViewFactory) { + public init( + checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase, + openNotificationSettingUseCase: OpenNotificationSettingUseCase, + updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase, + updateUserInfoUseCase: UpdateUserInfoUseCase, + dictionaryMainViewFactory: DictionaryMainViewFactory + ) { self.checkNotificationPermissionUseCase = checkNotificationPermissionUseCase self.openNotificationSettingUseCase = openNotificationSettingUseCase self.updateNotificationAgreementUseCase = updateNotificationAgreementUseCase @@ -20,7 +26,14 @@ public struct OnBoardingNotificationSheetFactoryImpl: OnBoardingNotificationShee public func make(selectedLevel: Int, selectedJobID: Int) -> BaseViewController & ModalPresentable { let viewController = OnBoardingNotificationSheetViewController(dictionaryMainViewFactory: dictionaryMainViewFactory) - viewController.reactor = OnBoardingNotificationSheetReactor(selectedLevel: selectedLevel, selectedJobID: selectedJobID, checkNotificationPermissionUseCase: checkNotificationPermissionUseCase, openNotificationSettingUseCase: openNotificationSettingUseCase, updateNotificationAgreementUseCase: updateNotificationAgreementUseCase, updateUserInfoUseCase: updateUserInfoUseCase) + viewController.reactor = OnBoardingNotificationSheetReactor( + selectedLevel: selectedLevel, + selectedJobID: selectedJobID, + checkNotificationPermissionUseCase: checkNotificationPermissionUseCase, + openNotificationSettingUseCase: openNotificationSettingUseCase, + updateNotificationAgreementUseCase: updateNotificationAgreementUseCase, + updateUserInfoUseCase: updateUserInfoUseCase + ) return viewController } } diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift index 0dd02188..3c72a9a3 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift @@ -46,7 +46,14 @@ public final class OnBoardingNotificationSheetReactor: Reactor { private let updateUserInfoUseCase: UpdateUserInfoUseCase // MARK: - init - public init(selectedLevel: Int, selectedJobID: Int, checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase, openNotificationSettingUseCase: OpenNotificationSettingUseCase, updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase, updateUserInfoUseCase: UpdateUserInfoUseCase) { + public init( + selectedLevel: Int, + selectedJobID: Int, + checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase, + openNotificationSettingUseCase: OpenNotificationSettingUseCase, + updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase, + updateUserInfoUseCase: UpdateUserInfoUseCase + ) { self.initialState = State(selectedLevel: selectedLevel, selectedJobID: selectedJobID) self.checkNotificationPermissionUseCase = checkNotificationPermissionUseCase self.openNotificationSettingUseCase = openNotificationSettingUseCase diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift index cce762ac..5738ad10 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift @@ -14,9 +14,10 @@ public final class OnBoardingNotificationSheetViewController: BaseViewController public typealias Reactor = OnBoardingNotificationSheetReactor - // MARK: - Properties public var disposeBag = DisposeBag() + // MARK: - Properties + private let dictionaryMainViewFactory: DictionaryMainViewFactory // MARK: - Components @@ -113,14 +114,7 @@ extension OnBoardingNotificationSheetViewController { case .home: let viewController = owner.dictionaryMainViewFactory.make() let navigationController = UINavigationController(rootViewController: viewController) - - if let window = UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .flatMap({ $0.windows }) - .first(where: { $0.isKeyWindow }) { - window.rootViewController = navigationController - window.makeKeyAndVisible() - } + AppRouter.setRoot(navigationController) case .setting: guard let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) else { return } diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingQuestion/OnBoardingQuestionViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingQuestion/OnBoardingQuestionViewController.swift index 6439b81a..c1dbcf47 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingQuestion/OnBoardingQuestionViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingQuestion/OnBoardingQuestionViewController.swift @@ -11,10 +11,12 @@ import SnapKit public class OnBoardingQuestionViewController: BaseViewController, View { // MARK: - Properties public typealias Reactor = OnBoardingQuestionReactor + + public var disposeBag = DisposeBag() + private let onBoardingInputFactory: OnBoardingInputFactory // MARK: - Components - public var disposeBag = DisposeBag() private var mainView = OnBoardingQuestionView() diff --git a/MLS/Presentation/AuthFeature/AuthFeatureDemo/AppDelegate.swift b/MLS/Presentation/AuthFeature/AuthFeatureDemo/AppDelegate.swift index 01e04687..36895d3e 100644 --- a/MLS/Presentation/AuthFeature/AuthFeatureDemo/AppDelegate.swift +++ b/MLS/Presentation/AuthFeature/AuthFeatureDemo/AppDelegate.swift @@ -39,25 +39,28 @@ private extension AppDelegate { func registerProvider() { DIContainer.register(type: NetworkProvider.self) { - return NetworkProviderImpl() + NetworkProviderImpl() } DIContainer.register(type: SocialAuthenticatableProvider.self, name: "kakao") { - return KakaoLoginProviderImpl() + KakaoLoginProviderImpl() } DIContainer.register(type: SocialAuthenticatableProvider.self, name: "apple") { - return AppleLoginProviderImpl() + AppleLoginProviderImpl() } } func registerRepository() { DIContainer.register(type: AuthAPIRepository.self) { - return AuthAPIRepositoryImpl( + AuthAPIRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), interceptor: TokenInterceptor(fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self)) ) } DIContainer.register(type: TokenRepository.self) { - return KeyChainRepositoryImpl() + KeyChainRepositoryImpl() + } + DIContainer.register(type: UserDefaultsRepository.self) { + UserDefaultsRepositoryImpl() } } @@ -71,121 +74,118 @@ private extension AppDelegate { return SocialLoginUseCaseImpl(provider: provider) } DIContainer.register(type: CheckEmptyLevelAndRoleUseCase.self) { - return CheckEmptyLevelAndRoleUseCaseImpl() + CheckEmptyLevelAndRoleUseCaseImpl() } DIContainer.register(type: CheckValidLevelUseCase.self) { - return CheckValidLevelUseCaseImpl() + CheckValidLevelUseCaseImpl() } DIContainer.register(type: FetchJobListUseCase.self) { - return FetchJobListUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + FetchJobListUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: LoginWithAppleUseCase.self) { - return LoginWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + LoginWithAppleUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self), userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self)) } DIContainer.register(type: LoginWithKakaoUseCase.self) { - return LoginWithKakaoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + LoginWithKakaoUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self), userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self)) } DIContainer.register(type: SignUpWithAppleUseCase.self) { - return SignUpWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + SignUpWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: SignUpWithKakaoUseCase.self) { - return SignUpWithKakaoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + SignUpWithKakaoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: UpdateUserInfoUseCase.self) { - return UpdateUserInfoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + UpdateUserInfoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: FetchTokenFromLocalUseCase.self) { - return FetchTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) + FetchTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: SaveTokenToLocalUseCase.self) { - return SaveTokenToLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) + SaveTokenToLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: DeleteTokenFromLocalUseCase.self) { - return DeleteTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) + DeleteTokenFromLocalUseCaseImpl(repository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: UpdateMarketingAgreementUseCase.self) { - return UpdateMarketingAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self)) + UpdateMarketingAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self)) } DIContainer.register(type: PutFCMTokenUseCase.self) { - return PutFCMTokenUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + PutFCMTokenUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: CheckNotificationPermissionUseCase.self) { - return CheckNotificationPermissionUseCaseImpl() + CheckNotificationPermissionUseCaseImpl() } DIContainer.register(type: OpenNotificationSettingUseCase.self) { - return OpenNotificationSettingUseCaseImpl() + OpenNotificationSettingUseCaseImpl() } DIContainer.register(type: UpdateNotificationAgreementUseCase.self) { - return UpdateNotificationAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self)) + UpdateNotificationAgreementUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: FetchDictionaryMapListUseCase.self) { - return FetchDictionaryMapListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryMapListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryItemListUseCase.self) { - return FetchDictionaryItemListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryItemListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryQuestListUseCase.self) { - return FetchDictionaryQuestListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryQuestListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryNpcListUseCase.self) { - return FetchDictionaryNpcListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + FetchDictionaryNpcListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryMonsterListUseCase.self) { - return FetchDictionaryMonsterListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) - } - DIContainer.register(type: FetchDictionaryItemsUseCase.self) { - return FetchDictionaryItemsUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListRepository.self)) + FetchDictionaryMonsterListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailMonsterUseCase.self) { - return FetchDictionaryDetailMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailMonsterItemsUseCase.self) { - return FetchDictionaryDetailMonsterDropItemUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailMonsterDropItemUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailMonsterMapUseCase.self) { - return FetchDictionaryDetailMonsterMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailMonsterMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailNpcUseCase.self) { - return FetchDictionaryDetailNpcUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailNpcUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailNpcQuestUseCase.self) { - return FetchDictionaryDetailNpcQuestUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailNpcQuestUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailNpcMapUseCase.self) { - return FetchDictionaryDetailNpcMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailNpcMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailItemUseCase.self) { - return FetchDictionaryDetailItemUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailItemUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailItemDropMonsterUseCase.self) { - return FetchDictionaryDetailItemDropMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailItemDropMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailQuestUseCase.self) { - return FetchDictionaryDetailQuestUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailQuestUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailQuestLinkedQuestsUseCase.self) { - return FetchDictionaryDetailQuestLinkedQuestsUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailQuestLinkedQuestsUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailMapUseCase.self) { - return FetchDictionaryDetailMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailMapUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailMapSpawnMonsterUseCase.self) { - return FetchDictionaryDetailMapSpawnMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) + FetchDictionaryDetailMapSpawnMonsterUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchDictionaryDetailMapNpcUseCase.self) { - return FetchDictionaryDetailMapNpcUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) - } - DIContainer.register(type: ToggleBookmarkUseCase.self) { - return ToggleBookmarkUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListRepository.self)) + FetchDictionaryDetailMapNpcUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } DIContainer.register(type: FetchNotificationUseCase.self) { - return FetchNotificationUseCaseImpl() + FetchNotificationUseCaseImpl() + } + DIContainer.register(type: FetchPlatformUseCase.self) { + FetchPlatformUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) } } func registerFactory() { DIContainer.register(type: OnBoardingNotificationSheetFactory.self) { - return OnBoardingNotificationSheetFactoryImpl( + OnBoardingNotificationSheetFactoryImpl( checkNotificationPermissionUseCase: DIContainer .resolve(type: CheckNotificationPermissionUseCase.self), openNotificationSettingUseCase: DIContainer @@ -195,23 +195,24 @@ private extension AppDelegate { ) } DIContainer.register(type: OnBoardingNotificationFactory.self) { - return OnBoardingNotificationFactoryImpl(onBoardingNotificationSheetFactory: DIContainer.resolve(type: OnBoardingNotificationSheetFactory.self)) + OnBoardingNotificationFactoryImpl(onBoardingNotificationSheetFactory: DIContainer.resolve(type: OnBoardingNotificationSheetFactory.self)) } DIContainer.register(type: OnBoardingInputFactory.self) { - return OnBoardingInputFactoryImpl( + OnBoardingInputFactoryImpl( checkEmptyUseCase: DIContainer.resolve(type: CheckEmptyLevelAndRoleUseCase.self), checkValidLevelUseCase: DIContainer.resolve(type: CheckValidLevelUseCase.self), fetchJobListUseCase: DIContainer.resolve(type: FetchJobListUseCase.self), updateUserInfoUseCase: DIContainer.resolve(type: UpdateUserInfoUseCase.self), - onBoardingNotificationFactory: DIContainer.resolve(type: OnBoardingNotificationFactory.self)) + onBoardingNotificationFactory: DIContainer.resolve(type: OnBoardingNotificationFactory.self) + ) } DIContainer.register(type: OnBoardingQuestionFactory.self) { - return OnBoardingQuestionFactoryImpl( + OnBoardingQuestionFactoryImpl( onBoardingInputFactory: DIContainer.resolve(type: OnBoardingInputFactory.self) ) } DIContainer.register(type: TermsAgreementFactory.self) { - return TermsAgreementFactoryImpl( + TermsAgreementFactoryImpl( onBoardingQuestionFactory: DIContainer.resolve(type: OnBoardingQuestionFactory.self), signUpWithKakaoUseCase: DIContainer.resolve(type: SignUpWithKakaoUseCase.self), signUpWithAppleUseCase: DIContainer.resolve(type: SignUpWithAppleUseCase.self), @@ -220,14 +221,15 @@ private extension AppDelegate { ) } DIContainer.register(type: LoginFactory.self) { - return LoginFactoryImpl( + LoginFactoryImpl( termsAgreementsFactory: DIContainer.resolve(type: TermsAgreementFactory.self), appleLoginUseCase: DIContainer.resolve(type: FetchSocialCredentialUseCase.self, name: "apple"), kakaoLoginUseCase: DIContainer.resolve(type: FetchSocialCredentialUseCase.self, name: "kakao"), loginWithAppleUseCase: DIContainer.resolve(type: LoginWithAppleUseCase.self), loginWithKakaoUseCase: DIContainer.resolve(type: LoginWithKakaoUseCase.self), fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self), - putFCMTokenUseCase: DIContainer.resolve(type: PutFCMTokenUseCase.self) + putFCMTokenUseCase: DIContainer.resolve(type: PutFCMTokenUseCase.self), + fetchPlatformUseCase: DIContainer.resolve(type: FetchPlatformUseCase.self) ) } } diff --git a/MLS/Presentation/AuthFeature/AuthFeatureDemo/ViewController.swift b/MLS/Presentation/AuthFeature/AuthFeatureDemo/ViewController.swift index d9d8a7de..75594d1d 100644 --- a/MLS/Presentation/AuthFeature/AuthFeatureDemo/ViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeatureDemo/ViewController.swift @@ -18,7 +18,7 @@ class ViewController: UIViewController { }() lazy var views: [UIViewController] = { - let loginVC = DIContainer.resolve(type: LoginFactory.self).make(isReLogin: false) + let loginVC = DIContainer.resolve(type: LoginFactory.self).make(exitRoute: .pop) loginVC.title = "로그인" let termVC = DIContainer.resolve(type: TermsAgreementFactory.self).make(credential: KakaoCredential(token: "", providerID: ""), platform: .apple) diff --git a/MLS/Presentation/AuthFeature/AuthFeatureInterface/LoginExitRoute.swift b/MLS/Presentation/AuthFeature/AuthFeatureInterface/LoginExitRoute.swift new file mode 100644 index 00000000..6807d771 --- /dev/null +++ b/MLS/Presentation/AuthFeature/AuthFeatureInterface/LoginExitRoute.swift @@ -0,0 +1,6 @@ +import UIKit + +public enum LoginExitRoute { + case pop + case home +} diff --git a/MLS/Presentation/AuthFeature/AuthFeatureInterface/LoginFactory.swift b/MLS/Presentation/AuthFeature/AuthFeatureInterface/LoginFactory.swift index 5d26770e..4231837f 100644 --- a/MLS/Presentation/AuthFeature/AuthFeatureInterface/LoginFactory.swift +++ b/MLS/Presentation/AuthFeature/AuthFeatureInterface/LoginFactory.swift @@ -1,5 +1,11 @@ import BaseFeature public protocol LoginFactory { - func make(isReLogin: Bool) -> BaseViewController + func make(exitRoute: LoginExitRoute, onLoginCompleted: (() -> Void)?) -> BaseViewController +} + +public extension LoginFactory { + func make(exitRoute: LoginExitRoute) -> BaseViewController { + make(exitRoute: exitRoute, onLoginCompleted: nil) + } } diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Base/BaseErrorViewController.swift b/MLS/Presentation/BaseFeature/BaseFeature/Base/BaseErrorViewController.swift index b4c701b9..6646537e 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Base/BaseErrorViewController.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Base/BaseErrorViewController.swift @@ -18,7 +18,7 @@ public final class BaseErrorViewController: BaseViewController { } // MARK: - Properties - var disposeBag = DisposeBag() + private var disposeBag = DisposeBag() private let containerView: UIView = UIView() diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/AppRouter.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/AppRouter.swift new file mode 100644 index 00000000..1a74585b --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/AppRouter.swift @@ -0,0 +1,20 @@ +import UIKit + +public enum AppRouter { + public static func setRoot(_ viewController: UIViewController, animated: Bool = true) { + guard let window = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .flatMap({ $0.windows }) + .first(where: { $0.isKeyWindow }) else { return } + + if animated { + UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: { + window.rootViewController = viewController + }) + } else { + window.rootViewController = viewController + } + + window.makeKeyAndVisible() + } +} diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/Extension/Int+.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/Extension/Int+.swift new file mode 100644 index 00000000..5443d97d --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/Extension/Int+.swift @@ -0,0 +1,5 @@ +extension Array where Element == Int { + public func changeKoreanDate() -> String { + return "\(self[0])년 \(self[1])월 \(self[2])일 \(self[3]):\(String(format: "%02d", self[4]))" + } +} diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/AddCollection/AddCollectionViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/AddCollection/AddCollectionViewController.swift index 5bb00700..72aff19c 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/AddCollection/AddCollectionViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/AddCollection/AddCollectionViewController.swift @@ -14,6 +14,7 @@ public final class AddCollectionViewController: BaseViewController, View { // MARK: - Properties public var disposeBag = DisposeBag() + public var onDismissWithMessage: ((BookmarkCollection?) -> Void)? // MARK: - Components diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift index 5dc2330f..2103c5b6 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift @@ -18,13 +18,14 @@ final class BookmarkEmptyView: UIView { private let mainLabel = UILabel() private let subLabel = UILabel() - private let button = CommonButton(style: .normal, title: "북마크하러 가기", disabledTitle: nil) + public let button = CommonButton() // MARK: - Init init() { super.init(frame: .zero) addViews() setupConstraints() + configureUI() } @available(*, unavailable) @@ -51,7 +52,7 @@ private extension BookmarkEmptyView { mainLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom) - make.centerX.equalToSuperview() + make.horizontalEdges.equalToSuperview() } subLabel.snp.makeConstraints { make in @@ -65,10 +66,14 @@ private extension BookmarkEmptyView { make.width.equalTo(Constant.buttonWidth) } } + + func configureUI() { + backgroundColor = .neutral100 + } } extension BookmarkEmptyView { - func setLabel(isLogin: Bool, buttonAction: @escaping () -> Void) { + func setLabel(isLogin: Bool) { imageView.image = DesignSystemAsset.image(named: "noShowList") mainLabel.attributedText = .makeStyledString( font: .h_xl_b, @@ -81,9 +86,5 @@ extension BookmarkEmptyView { ) button.updateTitle(title: isLogin ? "북마크하러 가기" : "로그인하러 가기") - button.removeTarget(nil, action: nil, for: .allEvents) - button.addAction(UIAction { _ in - buttonAction() - }, for: .touchUpInside) } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift index d6edd92f..dec82dda 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift @@ -10,6 +10,7 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { private let sortedFactory: SortedBottomSheetFactory private let bookmarkModalFactory: BookmarkModalFactory private let loginFactory: LoginFactory + private let dictionaryDetailFactory: DictionaryDetailFactory private let setBookmarkUseCase: SetBookmarkUseCase private let checkLoginUseCase: CheckLoginUseCase @@ -19,6 +20,7 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { private let fetchNPCBookmarkUseCase: FetchNPCBookmarkUseCase private let fetchQuestBookmarkUseCase: FetchQuestBookmarkUseCase private let fetchMapBookmarkUseCase: FetchMapBookmarkUseCase + private let collectionEditFactory: CollectionEditFactory public init( itemFilterFactory: ItemFilterBottomSheetFactory, @@ -26,6 +28,7 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { sortedFactory: SortedBottomSheetFactory, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory, + dictionaryDetailFactory: DictionaryDetailFactory, setBookmarkUseCase: SetBookmarkUseCase, checkLoginUseCase: CheckLoginUseCase, fetchBookmarkUseCase: FetchBookmarkUseCase, @@ -33,13 +36,15 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { fetchItemBookmarkUseCase: FetchItemBookmarkUseCase, fetchNPCBookmarkUseCase: FetchNPCBookmarkUseCase, fetchQuestBookmarkUseCase: FetchQuestBookmarkUseCase, - fetchMapBookmarkUseCase: FetchMapBookmarkUseCase + fetchMapBookmarkUseCase: FetchMapBookmarkUseCase, + collectionEditFactory: CollectionEditFactory ) { self.itemFilterFactory = itemFilterFactory self.monsterFilterFactory = monsterFilterFactory self.sortedFactory = sortedFactory self.bookmarkModalFactory = bookmarkModalFactory self.loginFactory = loginFactory + self.dictionaryDetailFactory = dictionaryDetailFactory self.setBookmarkUseCase = setBookmarkUseCase self.checkLoginUseCase = checkLoginUseCase self.fetchBookmarkUseCase = fetchBookmarkUseCase @@ -48,17 +53,30 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { self.fetchItemBookmarkUseCase = fetchItemBookmarkUseCase self.fetchQuestBookmarkUseCase = fetchQuestBookmarkUseCase self.fetchMapBookmarkUseCase = fetchMapBookmarkUseCase + self.collectionEditFactory = collectionEditFactory } public func make(type: DictionaryType, listType: DictionaryMainViewType) -> BaseViewController { - let reactor = BookmarkListReactor(type: type, checkLoginUseCase: checkLoginUseCase, setBookmarkUseCase: setBookmarkUseCase, fetchBookmarkUseCase: fetchBookmarkUseCase, fetchMonsterBookmarkUseCase: fetchMonsterBookmarkUseCase, fetchItemBookmarkUseCase: fetchItemBookmarkUseCase, fetchNPCBookmarkUseCase: fetchNPCBookmarkUseCase, fetchQuestBookmarkUseCase: fetchQuestBookmarkUseCase, fetchMapBookmarkUseCase: fetchMapBookmarkUseCase) + let reactor = BookmarkListReactor( + type: type, + checkLoginUseCase: checkLoginUseCase, + setBookmarkUseCase: setBookmarkUseCase, + fetchBookmarkUseCase: fetchBookmarkUseCase, + fetchMonsterBookmarkUseCase: fetchMonsterBookmarkUseCase, + fetchItemBookmarkUseCase: fetchItemBookmarkUseCase, + fetchNPCBookmarkUseCase: fetchNPCBookmarkUseCase, + fetchQuestBookmarkUseCase: fetchQuestBookmarkUseCase, + fetchMapBookmarkUseCase: fetchMapBookmarkUseCase + ) let viewController = BookmarkListViewController( reactor: reactor, itemFilterFactory: itemFilterFactory, monsterFilterFactory: monsterFilterFactory, sortedFactory: sortedFactory, bookmarkModalFactory: bookmarkModalFactory, - loginFactory: loginFactory + loginFactory: loginFactory, + dictionaryDetailFactory: dictionaryDetailFactory, + collectionEditFactory: collectionEditFactory ) if listType == .search { viewController.isBottomTabbarHidden = true diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift index 66e08ee4..795752fa 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift @@ -4,11 +4,21 @@ import ReactorKit import RxSwift public final class BookmarkListReactor: Reactor { - // MARK: - Route + // MARK: - Type public enum Route { case none case sort(DictionaryType) case filter(DictionaryType) + case detail(DictionaryType, Int) + case dictionary + case login + case edit + } + + enum ViewState: Equatable { + case loginWithData + case loginWithoutData + case logout } // MARK: - Action @@ -17,21 +27,23 @@ public final class BookmarkListReactor: Reactor { case toggleBookmark(Int, Bool) case sortButtonTapped case filterButtonTapped + case editButtonTapped case fetchList case sortOptionSelected(SortType) case filterOptionSelected(startLevel: Int, endLevel: Int) case undoLastDeletedBookmark + case dataTapped(Int) + case emptyButtonTapped } // MARK: - Mutation public enum Mutation { case setItems([BookmarkResponse]) - case showSortFilter - case showFilter case setLoginState(Bool) case setSort(SortType) case setFilter(start: Int, end: Int) case setLastDeletedBookmark(BookmarkResponse?) + case toNavagate(Route) } // MARK: - State @@ -44,6 +56,15 @@ public final class BookmarkListReactor: Reactor { var startLevel: Int? var endLevel: Int? var lastDeletedBookmark: BookmarkResponse? + var viewState: ViewState { + if !isLogin { + return .logout + } else if items.isEmpty { + return .loginWithoutData + } else { + return .loginWithData + } + } } public var initialState: State @@ -102,27 +123,26 @@ public final class BookmarkListReactor: Reactor { } case let .toggleBookmark(id, isSelected): - guard let type = currentState.type.toItemType, - let bookmarkItem = currentState.items.first(where: { $0.originalId == id }) else { return .empty() } + guard let bookmarkItem = currentState.items.first(where: { $0.originalId == id }) else { return .empty() } let saveDeletedMutation: Observable = isSelected ? .just(.setLastDeletedBookmark(bookmarkItem)) - : .just(.setLastDeletedBookmark(nil)) + : .just(.setLastDeletedBookmark(nil)) return saveDeletedMutation .concat( setBookmarkUseCase.execute( bookmarkId: isSelected ? bookmarkItem.bookmarkId : id, - isBookmark: isSelected ? .delete : .set(type) + isBookmark: isSelected ? .delete : .set(bookmarkItem.type) ) .andThen(fetchList()) ) case .sortButtonTapped: - return .just(.showSortFilter) + return .just(.toNavagate(.sort(currentState.type))) case .filterButtonTapped: - return .just(.showFilter) + return .just(.toNavagate(.filter(currentState.type))) case .fetchList: guard currentState.isLogin else { return .empty() } @@ -152,6 +172,18 @@ public final class BookmarkListReactor: Reactor { .just(.setLastDeletedBookmark(nil)) ]) ) + case .dataTapped(let index): + let item = currentState.items[index] + guard let type = item.type.toDictionaryType else { return .empty() } + return .just(.toNavagate(.detail(type, item.originalId))) + case .emptyButtonTapped: + if currentState.viewState == .logout { + return .just(.toNavagate(.login)) + } else { + return .just(.toNavagate(.dictionary)) + } + case .editButtonTapped: + return .just(.toNavagate(.edit)) } } @@ -204,12 +236,6 @@ public final class BookmarkListReactor: Reactor { case let .setItems(response): newState.items = response - case .showSortFilter: - newState.route = .sort(newState.type) - - case .showFilter: - newState.route = .filter(newState.type) - case let .setLoginState(isLogin): newState.isLogin = isLogin @@ -220,8 +246,10 @@ public final class BookmarkListReactor: Reactor { newState.startLevel = start newState.endLevel = end - case .setLastDeletedBookmark(let item): + case let .setLastDeletedBookmark(item): newState.lastDeletedBookmark = item + case .toNavagate(let route): + newState.route = route } return newState diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift index 30a0fbe3..e70894a3 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift @@ -4,22 +4,45 @@ import BaseFeature import DesignSystem final class BookmarkListView: BaseListView { + let bookmarkEmptyView: BookmarkEmptyView + // MARK: - Init - init(isFilterHidden: Bool) { - let editButton = TextButton() - let sortButton = BaseListView.makeSortButton(title: "가나다 순", tintColor: .textColor) - let filterButton = BaseListView.makeFilterButton(title: "필터", tintColor: .textColor) - let emptyView = BookmarkEmptyView() + init(isFilterHidden: Bool, bookmarkEmptyView: BookmarkEmptyView) { + let editButton = TextButton() + let sortButton = BaseListView.makeSortButton(title: "가나다 순", tintColor: .textColor) + let filterButton = BaseListView.makeFilterButton(title: "필터", tintColor: .textColor) + self.bookmarkEmptyView = bookmarkEmptyView + super.init( + editButton: editButton, + sortButton: sortButton, + filterButton: filterButton, + emptyView: bookmarkEmptyView, + isFilterHidden: isFilterHidden + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +extension BookmarkListView { + func updateView(state: BookmarkListReactor.ViewState) { + switch state { + case .loginWithData: + checkEmptyData(isEmpty: false) - super.init( - editButton: editButton, - sortButton: sortButton, - filterButton: filterButton, - emptyView: emptyView, - isFilterHidden: isFilterHidden - ) - } + case .loginWithoutData: + checkEmptyData(isEmpty: true) + if let emptyView = emptyView as? BookmarkEmptyView { + checkEmptyData(isEmpty: true) + emptyView.setLabel(isLogin: true) + } - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError() } + case .logout: + if let emptyView = emptyView as? BookmarkEmptyView { + checkEmptyData(isEmpty: true) + emptyView.setLabel(isLogin: false) + } + } + } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift index 25ae0aa1..ced8efbc 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift @@ -3,6 +3,7 @@ import UIKit import AuthFeatureInterface import BaseFeature import BookmarkFeatureInterface +import DesignSystem import DictionaryFeatureInterface import ReactorKit @@ -19,11 +20,14 @@ public final class BookmarkListViewController: BaseViewController, View { private let bookmarkModalFactory: BookmarkModalFactory private let sortedFactory: SortedBottomSheetFactory private let loginFactory: LoginFactory + private let dictionaryDetailFactory: DictionaryDetailFactory + private let collectionEditFactory: CollectionEditFactory private var selectedSortIndex = 0 // MARK: - Components private var mainView: BookmarkListView + private var emptyView = BookmarkEmptyView() public init( reactor: BookmarkListReactor, @@ -31,14 +35,18 @@ public final class BookmarkListViewController: BaseViewController, View { monsterFilterFactory: MonsterFilterBottomSheetFactory, sortedFactory: SortedBottomSheetFactory, bookmarkModalFactory: BookmarkModalFactory, - loginFactory: LoginFactory + loginFactory: LoginFactory, + dictionaryDetailFactory: DictionaryDetailFactory, + collectionEditFactory: CollectionEditFactory ) { self.itemFilterFactory = itemFilterFactory self.monsterFilterFactory = monsterFilterFactory self.sortedFactory = sortedFactory self.bookmarkModalFactory = bookmarkModalFactory self.loginFactory = loginFactory - self.mainView = BookmarkListView(isFilterHidden: reactor.currentState.type.isBookmarkSortHidden) + self.dictionaryDetailFactory = dictionaryDetailFactory + self.collectionEditFactory = collectionEditFactory + self.mainView = BookmarkListView(isFilterHidden: reactor.currentState.type.isBookmarkSortHidden, bookmarkEmptyView: emptyView) super.init() self.reactor = reactor } @@ -109,6 +117,16 @@ extension BookmarkListViewController { .map { Reactor.Action.filterButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) + + mainView.editButton?.rx.tap + .map { Reactor.Action.editButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + emptyView.button.rx.tap + .map { .emptyButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) } func bindViewState(reactor: Reactor) { @@ -117,10 +135,8 @@ extension BookmarkListViewController { .distinctUntilChanged() .withUnretained(self) .observe(on: MainScheduler.instance) - .bind(onNext: { owner, items in + .bind(onNext: { owner, _ in owner.mainView.listCollectionView.reloadData() - owner.mainView.isUserInteractionEnabled = !items.isEmpty - owner.mainView.checkEmptyData(isEmpty: items.isEmpty) }) .disposed(by: disposeBag) @@ -153,6 +169,20 @@ extension BookmarkListViewController { default: break } + case .detail(let type, let id): + let vc = owner.dictionaryDetailFactory.make(type: type, id: id) + owner.navigationController?.pushViewController(vc, animated: true) + case .login: + let vc = owner.loginFactory.make(exitRoute: .pop) + owner.navigationController?.pushViewController(vc, animated: true) + + case .dictionary: + if let tabBarController = owner.tabBarController as? BottomTabBarController { + tabBarController.selectTab(index: 0) + } + case .edit: + let viewController = owner.collectionEditFactory.make() + owner.navigationController?.pushViewController(viewController, animated: true) default: break } @@ -169,19 +199,12 @@ extension BookmarkListViewController { .disposed(by: disposeBag) reactor.state - .map(\.isLogin) + .map(\.viewState) .distinctUntilChanged() .withUnretained(self) - .bind(onNext: { owner, isLogin in - guard let emptyView = owner.mainView.emptyView as? BookmarkEmptyView else { return } - emptyView.setLabel(isLogin: isLogin, buttonAction: { - if isLogin { - owner.tabBarController?.selectedIndex = 0 - } else { - let viewController = owner.loginFactory.make(isReLogin: false) - owner.navigationController?.pushViewController(viewController, animated: true) - } - }) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, state in + owner.mainView.updateView(state: state) }) .disposed(by: disposeBag) } @@ -227,7 +250,7 @@ extension BookmarkListViewController: UICollectionViewDelegate, UICollectionView ctaText: "로그인 하기", cancelText: "취소", ctaAction: { - let viewController = self.loginFactory.make(isReLogin: false) + let viewController = self.loginFactory.make(exitRoute: .pop) self.navigationController?.pushViewController(viewController, animated: true) }, cancelAction: nil @@ -252,4 +275,8 @@ extension BookmarkListViewController: UICollectionViewDelegate, UICollectionView return cell } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + reactor?.action.onNext(.dataTapped(indexPath.item)) + } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift index b794ecfa..7526273c 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift @@ -8,6 +8,7 @@ public final class BookmarkMainReactor: Reactor { case search case onBoarding case notification + case edit } public enum Action { diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift index 20d9c6bd..1352a597 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift @@ -14,6 +14,7 @@ public final class BookmarkMainViewController: BaseViewController, View { // MARK: - Properties public var disposeBag = DisposeBag() + private let initialIndex: Int private lazy var currentPageIndex = BehaviorRelay(value: initialIndex) diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkModal/BookmarkModalViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkModal/BookmarkModalViewController.swift index 5b5bcb3d..1faa22d3 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkModal/BookmarkModalViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkModal/BookmarkModalViewController.swift @@ -12,12 +12,12 @@ public final class BookmarkModalViewController: BaseViewController, View { public typealias Reactor = BookmarkModalReactor // MARK: - Properties - private let addCollectionFactory: AddCollectionFactory + public var disposeBag = DisposeBag() public var onDismissWithMessage: ((BookmarkCollection?) -> Void)? public var onDismissWithCollections: (([BookmarkCollection?]) -> Void)? - public var disposeBag = DisposeBag() + private let addCollectionFactory: AddCollectionFactory // MARK: - Components private let mainView = BookmarkModalView() diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailEmptyView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailEmptyView.swift index 62eb0122..15387f47 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailEmptyView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailEmptyView.swift @@ -60,7 +60,6 @@ private extension CollectionDetailEmptyView { func setupConstraints() { imageView.snp.makeConstraints { make in -// make.top.equalToSuperview() make.centerX.equalToSuperview() make.size.equalTo(Constant.imageSize) } @@ -79,7 +78,6 @@ private extension CollectionDetailEmptyView { make.top.equalTo(subLabel.snp.bottom).offset(Constant.buttonSpacing) make.width.equalTo(Constant.buttonWidth) make.centerX.equalToSuperview() -// make.centerX.bottom.equalToSuperview() } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailFactoryImpl.swift index 0fee018b..efe6dce9 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailFactoryImpl.swift @@ -9,19 +9,22 @@ public final class CollectionDetailFactoryImpl: CollectionDetailFactory { private let collectionSettingFactory: CollectionSettingFactory private let addCollectionFactory: AddCollectionFactory private let collectionEditFactory: CollectionEditFactory + private let dictionaryDetailFactory: DictionaryDetailFactory public init( setBookmarkUseCase: SetBookmarkUseCase, bookmarkModalFactory: BookmarkModalFactory, collectionSettingFactory: CollectionSettingFactory, addCollectionFactory: AddCollectionFactory, - collectionEditFactory: CollectionEditFactory + collectionEditFactory: CollectionEditFactory, + dictionaryDetailFactory: DictionaryDetailFactory ) { self.setBookmarkUseCase = setBookmarkUseCase self.bookmarkModalFactory = bookmarkModalFactory self.collectionSettingFactory = collectionSettingFactory self.addCollectionFactory = addCollectionFactory self.collectionEditFactory = collectionEditFactory + self.dictionaryDetailFactory = dictionaryDetailFactory } public func make(collection: BookmarkCollection) -> BaseViewController { @@ -34,7 +37,8 @@ public final class CollectionDetailFactoryImpl: CollectionDetailFactory { bookmarkModalFactory: bookmarkModalFactory, collectionSettingFactory: collectionSettingFactory, addCollectionFactory: addCollectionFactory, - collectionEditFactory: collectionEditFactory + collectionEditFactory: collectionEditFactory, + dictionaryDetailFactory: dictionaryDetailFactory ) return viewController } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailReactor.swift index 271e4f55..c848a242 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailReactor.swift @@ -9,10 +9,11 @@ public final class CollectionDetailReactor: Reactor { case toMain case dismiss case edit + case detail(DictionaryType, Int) } public enum Action { - case viewDidAppear + case viewWillAppear case backButtonTapped case editButtonTapped case addButtonTapped @@ -20,6 +21,8 @@ public final class CollectionDetailReactor: Reactor { case toggleBookmark(Int, Bool) case selectSetting(CollectionSettingMenu) case changeName(String) + case undoLastDeletedBookmark + case dataTapped(Int) } public enum Mutation { @@ -27,17 +30,18 @@ public final class CollectionDetailReactor: Reactor { case setItems([DictionaryItem]) case setMenu(CollectionSettingMenu) case setName(String) + case setLastDeletedBookmark(BookmarkResponse?) } public struct State { @Pulse var route: Route - let type = DictionaryMainViewType.bookmark - var collection: BookmarkCollection + @Pulse var collectionMenu: CollectionSettingMenu? var sections: [String] { return type.pageTabList.map { $0.title } } - - @Pulse var collectionMenu: CollectionSettingMenu? + let type = DictionaryMainViewType.bookmark + var collection: BookmarkCollection + var lastDeletedBookmark: BookmarkResponse? } // MARK: - Properties @@ -55,26 +59,40 @@ public final class CollectionDetailReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .toggleBookmark(let id, let isSelected): -// return toggleBookmarkUseCase.execute(id: id, type: .total) -// .map { Mutation.setItems($0) } + // 북마크 설정 및 마지막 삭제 데이터 저장 후 컬렉션 데이터 가져오는 동작 필요 return .empty() case .backButtonTapped: return .just(.navigateTo(.dismiss)) case .editButtonTapped: return .just(.navigateTo(.edit)) - case .viewDidAppear: + case .viewWillAppear: // 데이터 불러오기? return .empty() case .addButtonTapped: // 컬렉션 추가 return .empty() case .bookmarkButtonTapped: - // 북마크로 이동 - return .empty() + return .just(.navigateTo(.toMain)) case .selectSetting(let menu): return .just(.setMenu(menu)) case .changeName(let name): return .just(.setName(name)) + case .undoLastDeletedBookmark: + guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } + return setBookmarkUseCase.execute( + bookmarkId: lastDeleted.originalId, + isBookmark: .set(lastDeleted.type) + ) + .andThen( + Observable.concat([ + // 불러오기 + .just(.setLastDeletedBookmark(nil)) + ]) + ) + case .dataTapped(let index): + let item = currentState.collection.items[index] + guard let type = item.type.toDictionaryType else { return .empty() } + return .just(.navigateTo(.detail(type, item.id))) } } @@ -89,6 +107,8 @@ public final class CollectionDetailReactor: Reactor { newState.collectionMenu = menu case .setName(let name): newState.collection.title = name + case .setLastDeletedBookmark(let bookmark): + newState.lastDeletedBookmark = bookmark } return newState } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailView.swift index 5dd158b1..8e5c532a 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailView.swift @@ -7,7 +7,8 @@ import SnapKit final class CollectionDetailView: UIView { // MARK: - Type enum Constant { - static let TopMargin: CGFloat = 12 + static let topMargin: CGFloat = 12 + static let collectionViewMargin: CGFloat = 24 } // MARK: - Components @@ -61,16 +62,16 @@ private extension CollectionDetailView { spacer.snp.makeConstraints { make in make.top.equalTo(navigation.snp.bottom) make.horizontalEdges.equalToSuperview() - make.height.equalTo(Constant.TopMargin) + make.height.equalTo(Constant.topMargin) } listCollectionView.snp.makeConstraints { make in - make.top.equalTo(spacer.snp.bottom) + make.top.equalTo(spacer.snp.bottom).offset(Constant.collectionViewMargin) make.horizontalEdges.bottom.equalToSuperview() } emptyContainerView.snp.makeConstraints { make in - make.top.equalTo(navigation.snp.bottom).offset(Constant.TopMargin) + make.top.equalTo(navigation.snp.bottom).offset(Constant.collectionViewMargin) make.horizontalEdges.bottom.equalTo(safeAreaLayoutGuide) } @@ -90,7 +91,7 @@ private extension CollectionDetailView { // MARK: - Methods extension CollectionDetailView { func isEmptyData(isEmpty: Bool) { - listCollectionView.isHidden = !isEmpty - emptyContainerView.isHidden = isEmpty + listCollectionView.isHidden = isEmpty + emptyContainerView.isHidden = !isEmpty } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift index 8d529a02..dfbcae9a 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift @@ -2,6 +2,7 @@ import UIKit import BaseFeature import BookmarkFeatureInterface +import DictionaryFeatureInterface import ReactorKit import RxCocoa @@ -13,24 +14,26 @@ public final class CollectionDetailViewController: BaseViewController, View { public typealias Reactor = CollectionDetailReactor // MARK: - Properties + public var disposeBag = DisposeBag() + private let bookmarkModalFactory: BookmarkModalFactory private let collectionSettingFactory: CollectionSettingFactory private let addCollectionFactory: AddCollectionFactory private let collectionEditFactory: CollectionEditFactory - - public var disposeBag = DisposeBag() + private let dictionaryDetailFactory: DictionaryDetailFactory private var selectedSortIndex = 0 // MARK: - Components private var mainView: CollectionDetailView - public init(reactor: CollectionDetailReactor, bookmarkModalFactory: BookmarkModalFactory, collectionSettingFactory: CollectionSettingFactory, addCollectionFactory: AddCollectionFactory, collectionEditFactory: CollectionEditFactory) { + public init(reactor: CollectionDetailReactor, bookmarkModalFactory: BookmarkModalFactory, collectionSettingFactory: CollectionSettingFactory, addCollectionFactory: AddCollectionFactory, collectionEditFactory: CollectionEditFactory, dictionaryDetailFactory: DictionaryDetailFactory) { self.mainView = CollectionDetailView(navTitle: reactor.currentState.collection.title) self.bookmarkModalFactory = bookmarkModalFactory self.collectionSettingFactory = collectionSettingFactory self.addCollectionFactory = addCollectionFactory self.collectionEditFactory = collectionEditFactory + self.dictionaryDetailFactory = dictionaryDetailFactory super.init() self.reactor = reactor navigationController?.navigationBar.isHidden = true @@ -88,6 +91,11 @@ extension CollectionDetailViewController { } func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + mainView.emptyView.bookmarkButton.rx.tap .map { Reactor.Action.bookmarkButtonTapped } .bind(to: reactor.action) @@ -165,6 +173,9 @@ extension CollectionDetailViewController { owner.reactor?.action.onNext(.selectSetting(menu)) }) owner.presentModal(viewController) + case .detail(let type, let id): + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + owner.navigationController?.pushViewController(viewController, animated: true) default: break } @@ -243,4 +254,8 @@ extension CollectionDetailViewController: UICollectionViewDelegate, UICollection return cell } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + reactor?.action.onNext(.dataTapped(indexPath.row)) + } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditFactoryImpl.swift index f5762e27..020c1c91 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditFactoryImpl.swift @@ -15,6 +15,7 @@ public final class CollectionEditFactoryImpl: CollectionEditFactory { let reactor = CollectionEditReactor() let viewController = CollectionEditViewController(bookmarkModalFactory: bookmarkModalFactory) viewController.reactor = reactor + viewController.isBottomTabbarHidden = true return viewController } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionList/CollectionListViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionList/CollectionListViewController.swift index 0947e0a2..902cc963 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionList/CollectionListViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionList/CollectionListViewController.swift @@ -10,12 +10,13 @@ public final class CollectionListViewController: BaseViewController, View { public typealias Reactor = CollectionListReactor // MARK: - Properties - private let addCollectionFactory: AddCollectionFactory - private let detailFactory: CollectionDetailFactory - public var disposeBag = DisposeBag() + public var onDismissWithMessage: ((BookmarkCollection?) -> Void)? + private let addCollectionFactory: AddCollectionFactory + private let detailFactory: CollectionDetailFactory + // MARK: - Components private var mainView = CollectionListView() diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionSettingSheet/CollectionSettingViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionSettingSheet/CollectionSettingViewController.swift index 4e2ab091..30f06bb2 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionSettingSheet/CollectionSettingViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionSettingSheet/CollectionSettingViewController.swift @@ -15,6 +15,7 @@ public final class CollectionSettingViewController: BaseViewController, ModalPre // MARK: - Properties public var disposeBag = DisposeBag() + public var setMenu: ((CollectionSettingMenu) -> Void)? private var mainView = CollectionSettingView() diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeatureDemo/AppDelegate.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeatureDemo/AppDelegate.swift index cd6b57c4..3dfa35d1 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeatureDemo/AppDelegate.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeatureDemo/AppDelegate.swift @@ -1,4 +1,5 @@ // swiftlint:disable function_body_length +// swiftlint:disable line_length import UIKit @@ -70,6 +71,9 @@ private extension AppDelegate { DIContainer.register(type: DictionaryDetailAPIRepository.self) { return DictionaryDetailAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), tokenInterceptor: TokenInterceptor(fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self))) } + DIContainer.register(type: UserDefaultsRepository.self) { + UserDefaultsRepositoryImpl() + } } func registerUseCase() { @@ -207,6 +211,24 @@ private extension AppDelegate { DIContainer.register(type: FetchBookmarkUseCase.self) { FetchBookmarkUseCaseImpl(repository: DIContainer.resolve(type: BookmarkRepository.self)) } + DIContainer.register(type: FetchDictionaryAllListUseCase.self) { + FetchDictionaryAllListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + } + DIContainer.register(type: RecentSearchRemoveUseCase.self) { + RecentSearchRemoveUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } + DIContainer.register(type: RecentSearchAddUseCase.self) { + RecentSearchAddUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } + DIContainer.register(type: FetchDictionaryListCountUseCase.self) { + FetchDictionaryListCountUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + } + DIContainer.register(type: FetchDictionarySearchListUseCase.self) { + FetchDictionarySearchListUseCaseImpl(repository: DIContainer.resolve(type: DictionaryListAPIRepository.self)) + } + DIContainer.register(type: RecentSearchFetchUseCase.self) { + RecentSearchFetchUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } } func registerFactory() { @@ -229,8 +251,9 @@ private extension AppDelegate { return DictionaryDetailFactoryImpl(dictionaryDetailMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapUseCase.self), dictionaryDetailMapSpawnMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapSpawnMonsterUseCase.self), dictionaryDetailMapNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapNpcUseCase.self), dictionaryDetailQuestLinkedQuestsUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestLinkedQuestsUseCase.self), dictionaryDetailQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestUseCase.self), dictionaryDetailItemDropMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemDropMonsterUseCase.self), dictionaryDetailItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemUseCase.self), dictionaryDetailNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcUseCase.self), dictionaryDetailNpcQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcQuestUseCase.self), dictionaryDetailNpcMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcMapUseCase.self), dictionaryDetailMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterUseCase.self), dictionaryDetailMonsterDropItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterItemsUseCase.self), dictionaryDetailMonsterMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterMapUseCase.self)) } DIContainer.register(type: DictionaryMainListFactory.self) { - return DictionaryListFactoryImpl( + DictionaryListFactoryImpl( checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), + dictionaryAllListItemUseCase: DIContainer.resolve(type: FetchDictionaryAllListUseCase.self), dictionaryMapListItemUseCase: DIContainer.resolve(type: FetchDictionaryMapListUseCase.self), dictionaryItemListItemUseCase: DIContainer.resolve(type: FetchDictionaryItemListUseCase.self), dictionaryQuestListItemUseCase: DIContainer.resolve(type: FetchDictionaryQuestListUseCase.self), @@ -246,14 +269,17 @@ private extension AppDelegate { } DIContainer.register(type: DictionarySearchResultFactory.self) { DictionarySearchResultFactoryImpl( + dictionaryListCountUseCase: DIContainer.resolve(type: FetchDictionaryListCountUseCase.self), dictionaryMainListFactory: DIContainer - .resolve(type: DictionaryMainListFactory.self) + .resolve(type: DictionaryMainListFactory.self), + dictionarySearchListUseCase: DIContainer.resolve(type: FetchDictionarySearchListUseCase.self) ) } DIContainer.register(type: DictionarySearchFactory.self) { - DictionarySearchFactoryImpl( + DictionarySearchFactoryImpl(recentSearchRemoveUseCase: DIContainer.resolve(type: RecentSearchRemoveUseCase.self), + recentSearchAddUseCase: DIContainer.resolve(type: RecentSearchAddUseCase.self), searchResultFactory: DIContainer - .resolve(type: DictionarySearchResultFactory.self) + .resolve(type: DictionarySearchResultFactory.self), recentSearchFetchUseCase: DIContainer.resolve(type: RecentSearchFetchUseCase.self) ) } DIContainer.register(type: NotificationSettingFactory.self) { @@ -273,8 +299,7 @@ private extension AppDelegate { .resolve(type: DictionaryMainListFactory.self), searchFactory: DIContainer.resolve(type: DictionarySearchFactory.self), notificationFactory: DIContainer - .resolve(type: DictionaryNotificationFactory.self) - ) + .resolve(type: DictionaryNotificationFactory.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self)) } DIContainer.register(type: BookmarkOnBoardingFactory.self) { BookmarkOnBoardingFactoryImpl() @@ -334,7 +359,7 @@ private extension AppDelegate { ) } DIContainer.register(type: BookmarkListFactory.self) { - BookmarkListFactoryImpl(itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), loginFactory: DIContainer.resolve(type: LoginFactory.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), fetchBookmarkUseCase: DIContainer.resolve(type: FetchBookmarkUseCase.self), fetchMonsterBookmarkUseCase: DIContainer.resolve(type: FetchMonsterBookmarkUseCase.self), fetchItemBookmarkUseCase: DIContainer.resolve(type: FetchItemBookmarkUseCase.self), fetchNPCBookmarkUseCase: DIContainer.resolve(type: FetchNPCBookmarkUseCase.self), fetchQuestBookmarkUseCase: DIContainer.resolve(type: FetchQuestBookmarkUseCase.self), fetchMapBookmarkUseCase: DIContainer.resolve(type: FetchMapBookmarkUseCase.self)) + BookmarkListFactoryImpl(itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), loginFactory: DIContainer.resolve(type: LoginFactory.self), dictionaryDetailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), fetchBookmarkUseCase: DIContainer.resolve(type: FetchBookmarkUseCase.self), fetchMonsterBookmarkUseCase: DIContainer.resolve(type: FetchMonsterBookmarkUseCase.self), fetchItemBookmarkUseCase: DIContainer.resolve(type: FetchItemBookmarkUseCase.self), fetchNPCBookmarkUseCase: DIContainer.resolve(type: FetchNPCBookmarkUseCase.self), fetchQuestBookmarkUseCase: DIContainer.resolve(type: FetchQuestBookmarkUseCase.self), fetchMapBookmarkUseCase: DIContainer.resolve(type: FetchMapBookmarkUseCase.self)) } DIContainer.register(type: CollectionSettingFactory.self) { CollectionSettingFactoryImpl() diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift index 3d264601..c2744498 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift @@ -84,6 +84,13 @@ public final class CommonButton: UIButton { configureUI() } + public init() { + self.style = .normal + self.title = nil + self.disabledTitle = nil + super.init(frame: .zero) + } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("\(#file), \(#function) Error") diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift index eb8ce922..adfb9941 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift @@ -95,4 +95,11 @@ public extension BottomTabBarController { divider.alpha = hidden ? 0 : 1 } } + + func selectTab(index: Int, animated: Bool = false) { + UIView.performWithoutAnimation { + selectedIndex = index + customTabBar.selectTab(index: index) + } + } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature.xcodeproj/project.pbxproj b/MLS/Presentation/DictionaryFeature/DictionaryFeature.xcodeproj/project.pbxproj index d6609960..32df0154 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature.xcodeproj/project.pbxproj +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 77C97DAB2E37D4AC007198DA /* AuthFeature.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77C97DAA2E37D4AC007198DA /* AuthFeature.framework */; }; 77DBD8BF2E1AD0C600529428 /* BookmarkFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77DBD8BE2E1AD0C600529428 /* BookmarkFeatureInterface.framework */; }; 77DBD8C02E1AD0C600529428 /* BookmarkFeatureInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 77DBD8BE2E1AD0C600529428 /* BookmarkFeatureInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 77FEEF1D2EC5B0870023197A /* AuthFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77FEEF1C2EC5B0870023197A /* AuthFeatureInterface.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -143,6 +144,7 @@ 77C974722E376124007198DA /* BookmarkFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BookmarkFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 77C97DAA2E37D4AC007198DA /* AuthFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 77DBD8BE2E1AD0C600529428 /* BookmarkFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BookmarkFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 77FEEF1C2EC5B0870023197A /* AuthFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -188,6 +190,7 @@ 081D9DC52DF80E15004F850D /* DomainInterface.framework in Frameworks */, 081D9DDA2DF80E7E004F850D /* ReactorKit in Frameworks */, 77C974732E376124007198DA /* BookmarkFeatureInterface.framework in Frameworks */, + 77FEEF1D2EC5B0870023197A /* AuthFeatureInterface.framework in Frameworks */, 081D9DD02DF80E5C004F850D /* RxCocoa in Frameworks */, 77C97DAB2E37D4AC007198DA /* AuthFeature.framework in Frameworks */, 081D9DC12DF80DF6004F850D /* BaseFeature.framework in Frameworks */, @@ -262,6 +265,7 @@ 081D9DBF2DF80DF6004F850D /* Frameworks */ = { isa = PBXGroup; children = ( + 77FEEF1C2EC5B0870023197A /* AuthFeatureInterface.framework */, 773E8D4F2E7AF4F60094714D /* MyPageFeatureInterface.framework */, 773E8D4C2E7AF4F00094714D /* MyPageFeature.framework */, 773E8D492E7AF4D30094714D /* MyPageFeature.framework */, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift index 73979d6d..f8e4f3ae 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift @@ -114,11 +114,7 @@ class DictionaryDetailBaseView: UIView { }() // 북마크 버튼 - public let bookmarkButton: UIButton = { - let button = UIButton() - button.setImage(DesignSystemAsset.image(named: "bookmarkGrayBorder"), for: .normal) - return button - }() + public let bookmarkButton = UIButton() // 이름 public let nameLabel: UILabel = { @@ -443,4 +439,8 @@ extension DictionaryDetailBaseView { tabBarStackView.addArrangedSubview(spacerView) tabBarStickyStackView.addArrangedSubview(stickySpacerView) } + + func setBookmark(isBookmarked: Bool) { + bookmarkButton.setImage(DesignSystemAsset.image(named: isBookmarked ? "bookmark" : "bookmarkGrayBorder"), for: .normal) + } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index beb3a61d..4e7fd8f4 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -1,6 +1,8 @@ import UIKit +import AuthFeatureInterface import BaseFeature +import BookmarkFeatureInterface import DesignSystem import DomainInterface @@ -26,14 +28,18 @@ class DictionaryDetailBaseViewController: BaseViewController { /// 현재 보여지고 있는 뷰의 인덱스 private var currentTabIndex: Int? + private let bookmarkModalFactory: BookmarkModalFactory + private let loginFactory: LoginFactory // MARK: - Components public var mainView = DictionaryDetailBaseView() // 타입설정 public var type: DictionaryItemType - public init(type: DictionaryItemType) { + public init(type: DictionaryItemType, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory) { self.type = type + self.bookmarkModalFactory = bookmarkModalFactory + self.loginFactory = loginFactory mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) super.init() isBottomTabbarHidden = true @@ -49,8 +55,8 @@ class DictionaryDetailBaseViewController: BaseViewController { addViews() setupConstraints() - bindActions() // 액션 바인딩 - mainView.scrollView.delegate = self + configureUI() + bind() // 액션 바인딩 setupMenu(type.detailTypes) } @@ -77,6 +83,10 @@ private extension DictionaryDetailBaseViewController { make.horizontalEdges.bottom.equalToSuperview() } } + + func configureUI() { + mainView.scrollView.delegate = self + } } /// 스티키 헤더 만들기 위한 델리게이트 @@ -141,7 +151,6 @@ extension DictionaryDetailBaseViewController { var currentRowWidth: CGFloat = 0 for (element, value) in tags.nonNilElements() { - let badge = Badge(style: .element("\(element.rawValue) \(value)")) let fittingSize = badge.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let badgeWidth = fittingSize.width @@ -236,13 +245,84 @@ extension DictionaryDetailBaseViewController { mainView.setTabView(index: index, contentViews: contentViews) currentTabIndex = index } + + func bindBookmarkButton( + buttonTap: ControlEvent, + currentItem: Observable, + isLogin: @escaping () -> Bool, + imageUrl: @escaping (T) -> String?, + backgroundColor: UIColor, + isBookmarked: @escaping (T) -> Bool, + toggleBookmark: @escaping (Bool) -> Void, + undoLastDeleted: @escaping () -> Void) -> Disposable { + buttonTap + .withLatestFrom(currentItem) + .observe(on: MainScheduler.instance) + .bind { [weak self] item in + guard let self else { return } + + guard isLogin() else { + GuideAlertFactory.show( + mainText: "북마크를 하려면 로그인이 필요해요.", + ctaText: "로그인 하기", + cancelText: "취소", + ctaAction: { + let viewController = self.loginFactory.make(exitRoute: .pop) + self.navigationController?.pushViewController(viewController, animated: true) + }, + cancelAction: nil + ) + return + } + + if isBookmarked(item) { + toggleBookmark(true) + SnackBarFactory.createSnackBar( + type: .delete, + imageUrl: imageUrl(item), + imageBackgroundColor: backgroundColor, + text: "아이템을 북마크에서 삭제했어요.", + buttonText: "되돌리기", + buttonAction: { undoLastDeleted() } + ) + } else { + toggleBookmark(false) + SnackBarFactory.createSnackBar( + type: .normal, + imageUrl: imageUrl(item), + imageBackgroundColor: backgroundColor, + text: "아이템을 북마크에 추가했어요.", + buttonText: "컬렉션 추가", + buttonAction: { [weak self] in + guard let self else { return } + DispatchQueue.main.async { + let viewController = self.bookmarkModalFactory.make( + onDismissWithColletions: { _ in }, + onDismissWithMessage: { _ in + ToastFactory.createToast( + message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." + ) + } + ) + viewController.modalPresentationStyle = .pageSheet + if let sheet = viewController.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 16 + } + self.present(viewController, animated: true) + } + } + ) + } + } + } } private extension DictionaryDetailBaseViewController { - func bindActions() { + func bind() { // 뒤로가기 버튼 액션 바인드 bindBackButton() - bindBookmarkButton() } func bindBackButton() { @@ -252,15 +332,6 @@ private extension DictionaryDetailBaseViewController { } .disposed(by: disposeBag) } - - // 이부분은 왜 inject로 넣어야 하나?? - func bindBookmarkButton() { - mainView.bookmarkButton.rx.tap - .bind { [weak self] in - print("bookmark tapped") - } - .disposed(by: disposeBag) - } } extension DictionaryDetailBaseViewController { @@ -286,5 +357,4 @@ extension DictionaryDetailBaseViewController { } .disposed(by: disposeBag) } - } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift index 8aa664e7..2b672093 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift @@ -1,8 +1,12 @@ +import AuthFeatureInterface import BaseFeature +import BookmarkFeatureInterface import DictionaryFeatureInterface import DomainInterface public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { + private let loginFactory: () -> LoginFactory + private let bookmarkModalFactory: BookmarkModalFactory private let dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase private let dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase private let dictionaryDetailMapNpcUseCase: FetchDictionaryDetailMapNpcUseCase @@ -17,8 +21,30 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { private let dictionaryDetailMonsterUseCase: FetchDictionaryDetailMonsterUseCase private let dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase private let dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let setBookmarkUseCase: SetBookmarkUseCase - public init(dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase, dictionaryDetailMapNpcUseCase: FetchDictionaryDetailMapNpcUseCase, dictionaryDetailQuestLinkedQuestsUseCase: FetchDictionaryDetailQuestLinkedQuestsUseCase, dictionaryDetailQuestUseCase: FetchDictionaryDetailQuestUseCase, dictionaryDetailItemDropMonsterUseCase: FetchDictionaryDetailItemDropMonsterUseCase, dictionaryDetailItemUseCase: FetchDictionaryDetailItemUseCase, dictionaryDetailNpcUseCase: FetchDictionaryDetailNpcUseCase, dictionaryDetailNpcQuestUseCase: FetchDictionaryDetailNpcQuestUseCase, dictionaryDetailNpcMapUseCase: FetchDictionaryDetailNpcMapUseCase, dictionaryDetailMonsterUseCase: FetchDictionaryDetailMonsterUseCase, dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase, dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase) { + public init( + loginFactory: @escaping () -> LoginFactory, + bookmarkModalFactory: BookmarkModalFactory, + dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase, + dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase, + dictionaryDetailMapNpcUseCase: FetchDictionaryDetailMapNpcUseCase, + dictionaryDetailQuestLinkedQuestsUseCase: FetchDictionaryDetailQuestLinkedQuestsUseCase, + dictionaryDetailQuestUseCase: FetchDictionaryDetailQuestUseCase, + dictionaryDetailItemDropMonsterUseCase: FetchDictionaryDetailItemDropMonsterUseCase, + dictionaryDetailItemUseCase: FetchDictionaryDetailItemUseCase, + dictionaryDetailNpcUseCase: FetchDictionaryDetailNpcUseCase, + dictionaryDetailNpcQuestUseCase: FetchDictionaryDetailNpcQuestUseCase, + dictionaryDetailNpcMapUseCase: FetchDictionaryDetailNpcMapUseCase, + dictionaryDetailMonsterUseCase: FetchDictionaryDetailMonsterUseCase, + dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase, + dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase, + checkLoginUseCase: CheckLoginUseCase, + setBookmarkUseCase: SetBookmarkUseCase + ) { + self.loginFactory = loginFactory + self.bookmarkModalFactory = bookmarkModalFactory self.dictionaryDetailMapUseCase = dictionaryDetailMapUseCase self.dictionaryDetailMapSpawnMonsterUseCase = dictionaryDetailMapSpawnMonsterUseCase self.dictionaryDetailMapNpcUseCase = dictionaryDetailMapNpcUseCase @@ -32,6 +58,8 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { self.dictionaryDetailMonsterUseCase = dictionaryDetailMonsterUseCase self.dictionaryDetailMonsterDropItemUseCase = dictionaryDetailMonsterDropItemUseCase self.dictionaryDetailMonsterMapUseCase = dictionaryDetailMonsterMapUseCase + self.checkLoginUseCase = checkLoginUseCase + self.setBookmarkUseCase = setBookmarkUseCase } public func make(type: DictionaryType, id: Int) -> BaseViewController { @@ -42,32 +70,53 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { case .collection: break case .item: - viewController = ItemDictionaryDetailViewController(type: .item) - let reactor = ItemDictionaryDetailReactor(dictionaryDetailItemUseCase: dictionaryDetailItemUseCase, dictionaryDetailItemDropMonsterUseCase: dictionaryDetailItemDropMonsterUseCase, id: id) + viewController = ItemDictionaryDetailViewController(type: .item, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory()) + let reactor = ItemDictionaryDetailReactor(dictionaryDetailItemUseCase: dictionaryDetailItemUseCase, dictionaryDetailItemDropMonsterUseCase: dictionaryDetailItemDropMonsterUseCase, checkLoginUseCase: checkLoginUseCase, setBookmarkUseCase: setBookmarkUseCase, id: id) if let viewController = viewController as? ItemDictionaryDetailViewController { viewController.reactor = reactor } case .monster: - viewController = MonsterDictionaryDetailViewController(type: .monster) - let reactor = MonsterDictionaryDetailReactor(dictionaryDetailMonsterUseCase: dictionaryDetailMonsterUseCase, dictionaryDetailMonsterDropItemUseCase: dictionaryDetailMonsterDropItemUseCase, dictionaryDetailMonsterMapUseCase: dictionaryDetailMonsterMapUseCase, id: id) + viewController = MonsterDictionaryDetailViewController(type: .monster, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory()) + let reactor = MonsterDictionaryDetailReactor( + dictionaryDetailMonsterUseCase: dictionaryDetailMonsterUseCase, + dictionaryDetailMonsterDropItemUseCase: dictionaryDetailMonsterDropItemUseCase, + dictionaryDetailMonsterMapUseCase: dictionaryDetailMonsterMapUseCase, + checkLoginUseCase: checkLoginUseCase, + setBookmarkUseCase: setBookmarkUseCase, + id: id + ) if let viewController = viewController as? MonsterDictionaryDetailViewController { viewController.reactor = reactor } case .map: - let reactor = MapDictionaryDetailReactor(dictionaryDetailMapUseCase: dictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: dictionaryDetailMapSpawnMonsterUseCase, dictionaryDetailMapNpcUseCase: dictionaryDetailMapNpcUseCase, id: id) - viewController = MapDictionaryDetailViewController(imageUrl: "") + let reactor = MapDictionaryDetailReactor( + dictionaryDetailMapUseCase: dictionaryDetailMapUseCase, + dictionaryDetailMapSpawnMonsterUseCase: dictionaryDetailMapSpawnMonsterUseCase, + dictionaryDetailMapNpcUseCase: dictionaryDetailMapNpcUseCase, + checkLoginUseCase: checkLoginUseCase, + setBookmarkUseCase: setBookmarkUseCase, + id: id + ) + viewController = MapDictionaryDetailViewController(type: .map, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory()) if let viewController = viewController as? MapDictionaryDetailViewController { viewController.reactor = reactor } case .npc: - let reactor = NpcDictionaryDetailReactor(dictionaryDetailNpcUseCase: dictionaryDetailNpcUseCase, dictionaryDetailNpcQuestUseCase: dictionaryDetailNpcQuestUseCase, dictionaryDetailNpcMapUseCase: dictionaryDetailNpcMapUseCase, id: id) - viewController = NpcDictionaryDetailViewController(imageUrl: "") + let reactor = NpcDictionaryDetailReactor( + dictionaryDetailNpcUseCase: dictionaryDetailNpcUseCase, + dictionaryDetailNpcQuestUseCase: dictionaryDetailNpcQuestUseCase, + dictionaryDetailNpcMapUseCase: dictionaryDetailNpcMapUseCase, + checkLoginUseCase: checkLoginUseCase, + setBookmarkUseCase: setBookmarkUseCase, + id: id + ) + viewController = NpcDictionaryDetailViewController(type: .npc, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory()) if let viewController = viewController as? NpcDictionaryDetailViewController { viewController.reactor = reactor } case .quest: - viewController = QuestDictionaryDetailViewController(type: .quest) - let reactor = QuestDictionaryDetailReactor(dictionaryDetailQuestUseCase: dictionaryDetailQuestUseCase, dictionaryDetailQuestLinkedQuestsUseCase: dictionaryDetailQuestLinkedQuestsUseCase, id: id) + viewController = QuestDictionaryDetailViewController(type: .quest, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory()) + let reactor = QuestDictionaryDetailReactor(dictionaryDetailQuestUseCase: dictionaryDetailQuestUseCase, dictionaryDetailQuestLinkedQuestUseCase: dictionaryDetailQuestLinkedQuestsUseCase, checkLoginUseCase: checkLoginUseCase, setBookmarkUseCase: setBookmarkUseCase, id: id) if let viewController = viewController as? QuestDictionaryDetailViewController { viewController.reactor = reactor } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift index 9c7d8928..83e8b92e 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift @@ -2,71 +2,137 @@ import DomainInterface import ReactorKit +// MARK: - Reactor public final class ItemDictionaryDetailReactor: Reactor { - // MARK: - Reactor + // MARK: Type public enum Route { case none case filter(DictionaryType) } - public let dictionaryDetailItemUseCase: FetchDictionaryDetailItemUseCase - public let dictionaryDetailItemDropMonsterUseCase: FetchDictionaryDetailItemDropMonsterUseCase - - public struct State { - @Pulse var route: Route = .none - var itemDetailInfo: DictionaryDetailItemResponse - var type: DictionaryType = .item - var monsters: [DictionaryDetailItemDropMonsterResponse] - var id = 0 - } - + // MARK: Action public enum Action { case filterButtonTapped case viewWillAppear case selectFilter(SortType) + case toggleBookmark(Bool) + case undoLastDeletedBookmark } + // MARK: Mutation public enum Mutation { case showFilter case setDetailData(DictionaryDetailItemResponse) case setDetailDropMonsterData([DictionaryDetailItemDropMonsterResponse]) + case setBookmark(DictionaryDetailItemResponse) + case setLastDeletedBookmark(DictionaryDetailItemResponse?) + case setLoginState(Bool) + } + + // MARK: State + public struct State { + @Pulse var route: Route = .none + var itemDetailInfo: DictionaryDetailItemResponse + var type: DictionaryType = .item + var monsters: [DictionaryDetailItemDropMonsterResponse] + var id: Int + var isLogin = false + var lastDeletedBookmark: DictionaryDetailItemResponse? } public var initialState: State private let disposeBag = DisposeBag() - public init(dictionaryDetailItemUseCase: FetchDictionaryDetailItemUseCase, dictionaryDetailItemDropMonsterUseCase: FetchDictionaryDetailItemDropMonsterUseCase, id: Int) { - self.initialState = .init(itemDetailInfo: DictionaryDetailItemResponse(itemId: nil, nameKr: nil, nameEn: nil, descriptionText: nil, imgUrl: nil, npcPrice: nil, itemType: nil, categoryHierachy: nil, availableJobs: nil, requiredStats: nil, equipmentStats: nil, scrollDetail: nil, bookmarkId: nil), type: .item, monsters: [], id: id) + private let dictionaryDetailItemUseCase: FetchDictionaryDetailItemUseCase + private let dictionaryDetailItemDropMonsterUseCase: FetchDictionaryDetailItemDropMonsterUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let setBookmarkUseCase: SetBookmarkUseCase + + // MARK: Init + public init( + dictionaryDetailItemUseCase: FetchDictionaryDetailItemUseCase, + dictionaryDetailItemDropMonsterUseCase: FetchDictionaryDetailItemDropMonsterUseCase, + checkLoginUseCase: CheckLoginUseCase, + setBookmarkUseCase: SetBookmarkUseCase, + id: Int + ) { self.dictionaryDetailItemUseCase = dictionaryDetailItemUseCase self.dictionaryDetailItemDropMonsterUseCase = dictionaryDetailItemDropMonsterUseCase + self.checkLoginUseCase = checkLoginUseCase + self.setBookmarkUseCase = setBookmarkUseCase + self.initialState = .init( + itemDetailInfo: DictionaryDetailItemResponse( + itemId: nil, nameKr: nil, nameEn: nil, descriptionText: nil, + imgUrl: nil, npcPrice: nil, itemType: nil, categoryHierachy: nil, + availableJobs: nil, requiredStats: nil, equipmentStats: nil, + scrollDetail: nil, bookmarkId: nil + ), + type: .item, + monsters: [], + id: id + ) } + // MARK: Mutate public func mutate(action: Action) -> Observable { switch action { case .filterButtonTapped: - return Observable.just(.showFilter) + return .just(.showFilter) case .viewWillAppear: return .merge([ - dictionaryDetailItemUseCase.execute(id: currentState.id).map {.setDetailData($0)}, - dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: nil).map {.setDetailDropMonsterData($0)} + checkLoginUseCase.execute().map { .setLoginState($0) }, + dictionaryDetailItemUseCase.execute(id: currentState.id).map { .setDetailData($0) }, + dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: nil).map { .setDetailDropMonsterData($0) } ]) case let .selectFilter(type): switch type { - case .mostDrop:// 드롭률 내림차순 - return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["dropRate", "desc"]).map {.setDetailDropMonsterData($0)} + case .mostDrop: + return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["dropRate", "desc"]).map { .setDetailDropMonsterData($0) } case .levelDESC: - return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["level", "desc"]).map {.setDetailDropMonsterData($0)} + return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["level", "desc"]).map { .setDetailDropMonsterData($0) } case .levelASC: - return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["level", "asc"]).map {.setDetailDropMonsterData($0)} + return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["level", "asc"]).map { .setDetailDropMonsterData($0) } default: - return Observable.empty() + return .empty() } + case let .toggleBookmark(isSelected): + guard let itemId = currentState.itemDetailInfo.itemId else { return .empty() } + + let saveDeleted: Observable = isSelected + ? .just(.setLastDeletedBookmark(currentState.itemDetailInfo)) + : .just(.setLastDeletedBookmark(nil)) + + return saveDeleted.concat( + setBookmarkUseCase.execute( + bookmarkId: currentState.itemDetailInfo.bookmarkId ?? itemId, + isBookmark: isSelected ? .delete : .set(.item) + ) + .andThen( + dictionaryDetailItemUseCase.execute(id: currentState.id) + .map { .setDetailData($0) } + ) + ) + case .undoLastDeletedBookmark: + guard let lastDeleted = currentState.lastDeletedBookmark, + let itemId = lastDeleted.itemId else { return .empty() } + + return setBookmarkUseCase.execute( + bookmarkId: itemId, + isBookmark: .set(.item) + ) + .andThen( + Observable.concat([ + dictionaryDetailItemUseCase.execute(id: currentState.id) + .map { .setDetailData($0) }, + .just(.setLastDeletedBookmark(nil)) + ]) + ) } } + // MARK: Reduce public func reduce(state: State, mutation: Mutation) -> State { var newState = state - switch mutation { case .showFilter: newState.route = .filter(newState.type) @@ -74,8 +140,13 @@ public final class ItemDictionaryDetailReactor: Reactor { newState.itemDetailInfo = data case let .setDetailDropMonsterData(data): newState.monsters = data + case let .setBookmark(item): + newState.itemDetailInfo = item + case let .setLastDeletedBookmark(item): + newState.lastDeletedBookmark = item + case let .setLoginState(isLogin): + newState.isLogin = isLogin } - return newState } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift index ffd6edfd..49ed775a 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift @@ -1,5 +1,6 @@ import UIKit +import BaseFeature import DesignSystem import DictionaryFeatureInterface import DomainInterface @@ -13,14 +14,13 @@ final class ItemDictionaryDetailViewController: DictionaryDetailBaseViewControll private let detailInfoView = DetailStackInfoView(type: .item) private let monsterCardView = DetailStackCardView() private let sortedFactory: SortedBottomSheetFactory = SortedBottomSheetFactoryImpl() - } // MARK: - Populate Data private extension ItemDictionaryDetailViewController { func setupMainInfo() { // 상세 정보(메인?) - self.inject(input: DictionaryDetailBaseViewController.Input( + inject(input: DictionaryDetailBaseViewController.Input( imageUrl: reactor?.currentState.itemDetailInfo.imgUrl, backgroundColor: type.backgroundColor, name: reactor?.currentState.itemDetailInfo.nameKr ?? "이름 없음", @@ -155,39 +155,49 @@ extension ItemDictionaryDetailViewController { bindUserAction(reactor: reactor) bindViewState(reactor: reactor) bindReportButton( - providerId: reactor.state.map { $0.itemDetailInfo.itemId ?? 0 }, - itemName: reactor.state.map { $0.itemDetailInfo.nameKr ?? "" } - ) + providerId: reactor.state.map { $0.itemDetailInfo.itemId ?? 0 }, + itemName: reactor.state.map { $0.itemDetailInfo.nameKr ?? "" } + ) } private func bindUserAction(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + monsterCardView.filterButton.rx.tap - .map { Reactor.Action.filterButtonTapped } - .bind(to: reactor.action) - .disposed(by: disposeBag) + .map { Reactor.Action.filterButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) } private func bindViewState(reactor: Reactor) { reactor.state.map(\.itemDetailInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setupMainInfo() - self?.setUpInfoStackView() + .withUnretained(self) + .bind(onNext: { owner, item in + owner.setupMainInfo() + owner.setUpInfoStackView() + owner.mainView.setBookmark(isBookmarked: item.bookmarkId != nil) }) .disposed(by: disposeBag) + reactor.state.map(\.monsters) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpMonsterView() + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.setUpMonsterView() }) .disposed(by: disposeBag) + rx.viewDidAppear .take(1) - .flatMapLatest { _ in return reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 + .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 .withUnretained(self) - .subscribe { (owner, route) in + .subscribe { owner, route in switch route { case .filter(let type): let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: owner.selectedIndex) { index in @@ -203,9 +213,16 @@ extension ItemDictionaryDetailViewController { } .disposed(by: disposeBag) - rx.viewWillAppear.take(1).subscribe { _ in - reactor.action.onNext(.viewWillAppear) - } + bindBookmarkButton( + buttonTap: mainView.bookmarkButton.rx.tap, + currentItem: reactor.state.map { $0.itemDetailInfo }, + isLogin: { reactor.currentState.isLogin }, + imageUrl: { $0.imgUrl }, + backgroundColor: type.backgroundColor, + isBookmarked: { $0.bookmarkId != nil }, + toggleBookmark: { isDeleting in reactor.action.onNext(.toggleBookmark(isDeleting)) }, + undoLastDeleted: { reactor.action.onNext(.undoLastDeletedBookmark) } + ) .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift index 08c4c543..0ed69b92 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift @@ -11,6 +11,8 @@ public final class MapDictionaryDetailReactor: Reactor { public enum Action { case filterButtonTapped case viewWillAppear + case toggleBookmark(Bool) + case undoLastDeletedBookmark } public enum Mutation { @@ -18,11 +20,16 @@ public final class MapDictionaryDetailReactor: Reactor { case setDetailData(DictionaryDetailMapResponse) case setDetailSpawnMonsters([DictionaryDetailMapSpawnMonsterResponse]) case setDetailNpc([DictionaryDetailMapNpcResponse]) + case setBookmark(DictionaryDetailMapResponse) + case setLastDeletedBookmark(DictionaryDetailMapResponse?) + case setLoginState(Bool) } public let dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase public let dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase public let dictionaryDetailMapNpcUseCase: FetchDictionaryDetailMapNpcUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let setBookmarkUseCase: SetBookmarkUseCase public struct State { @Pulse var route: Route = .none @@ -31,17 +38,28 @@ public final class MapDictionaryDetailReactor: Reactor { var npcs: [DictionaryDetailMapNpcResponse] var type: DictionaryType = .map var id = 0 + var isLogin = false + var lastDeletedBookmark: DictionaryDetailMapResponse? } public var initialState: State private let disposBag = DisposeBag() - public init(dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase, dictionaryDetailMapNpcUseCase: FetchDictionaryDetailMapNpcUseCase, id: Int) { + public init( + dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase, + dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase, + dictionaryDetailMapNpcUseCase: FetchDictionaryDetailMapNpcUseCase, + checkLoginUseCase: CheckLoginUseCase, + setBookmarkUseCase: SetBookmarkUseCase, + id: Int + ) { initialState = State(mapDetailInfo: DictionaryDetailMapResponse(mapId: nil, nameKr: nil, nameEn: nil, regionName: nil, detailName: nil, topRegionName: nil, mapUrl: nil, iconUrl: nil, bookmarkId: nil), spawnMonsters: [], npcs: [], type: .map, id: id) self.dictionaryDetailMapUseCase = dictionaryDetailMapUseCase self.dictionaryDetailMapSpawnMonsterUseCase = dictionaryDetailMapSpawnMonsterUseCase self.dictionaryDetailMapNpcUseCase = dictionaryDetailMapNpcUseCase + self.checkLoginUseCase = checkLoginUseCase + self.setBookmarkUseCase = setBookmarkUseCase } public func mutate(action: Action) -> Observable { @@ -50,10 +68,43 @@ public final class MapDictionaryDetailReactor: Reactor { return Observable.just(.showFilter) case .viewWillAppear: return .merge([ + checkLoginUseCase.execute().map { .setLoginState($0) }, dictionaryDetailMapUseCase.execute(id: currentState.id).map {.setDetailData($0)}, dictionaryDetailMapSpawnMonsterUseCase.execute(id: currentState.id).map {.setDetailSpawnMonsters($0)}, dictionaryDetailMapNpcUseCase.execute(id: currentState.id).map {.setDetailNpc($0)} ]) + case let .toggleBookmark(isSelected): + guard let itemId = currentState.mapDetailInfo.mapId else { return .empty() } + + let saveDeleted: Observable = isSelected + ? .just(.setLastDeletedBookmark(currentState.mapDetailInfo)) + : .just(.setLastDeletedBookmark(nil)) + + return saveDeleted.concat( + setBookmarkUseCase.execute( + bookmarkId: currentState.mapDetailInfo.bookmarkId ?? itemId, + isBookmark: isSelected ? .delete : .set(.map) + ) + .andThen( + dictionaryDetailMapUseCase.execute(id: currentState.id) + .map { .setDetailData($0) } + ) + ) + case .undoLastDeletedBookmark: + guard let lastDeleted = currentState.lastDeletedBookmark, + let mapId = lastDeleted.mapId else { return .empty() } + + return setBookmarkUseCase.execute( + bookmarkId: mapId, + isBookmark: .set(.map) + ) + .andThen( + Observable.concat([ + dictionaryDetailMapUseCase.execute(id: currentState.id) + .map { .setDetailData($0) }, + .just(.setLastDeletedBookmark(nil)) + ]) + ) } } @@ -69,6 +120,12 @@ public final class MapDictionaryDetailReactor: Reactor { newState.spawnMonsters = data case let .setDetailNpc(data): newState.npcs = data + case let .setBookmark(map): + newState.mapDetailInfo = map + case let .setLastDeletedBookmark(map): + newState.lastDeletedBookmark = map + case let .setLoginState(isLogin): + newState.isLogin = isLogin } return newState } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift index ba54c673..69e14300 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift @@ -1,5 +1,6 @@ import UIKit +import BaseFeature import DesignSystem import DictionaryFeatureInterface import DomainInterface @@ -12,7 +13,7 @@ final class MapDictionaryDetailViewController: DictionaryDetailBaseViewControlle public typealias Reactor = MapDictionaryDetailReactor // MARK: - Componenets - private var mapInfoView: DetailStackMapView + private var mapInfoView = DetailStackMapView(imageUrl: "") private var appearMonsterView = DetailStackCardView() private var appearNpcView = DetailStackCardView() private let sortedFactory: SortedBottomSheetFactory = SortedBottomSheetFactoryImpl() @@ -21,12 +22,6 @@ final class MapDictionaryDetailViewController: DictionaryDetailBaseViewControlle super.viewDidLoad() bindImageView() } - - init(imageUrl: String) { - self.mapInfoView = DetailStackMapView(imageUrl: imageUrl) - - super.init(type: .map) - } } // MARK: - SetUp @@ -34,7 +29,7 @@ private extension MapDictionaryDetailViewController { func setUpMainInfo() { guard let reactor = reactor else { return } let info = reactor.currentState.mapDetailInfo - self.inject( + inject( input: DictionaryDetailBaseViewController .Input( imageUrl: info.iconUrl, @@ -48,6 +43,7 @@ private extension MapDictionaryDetailViewController { func setUpMapView() { guard let reactor = reactor else { return } + mapInfoView.setUpMapView(imageUrl: reactor.currentState.mapDetailInfo.mapUrl) contentViews.append(mapInfoView) if let mapUrl = reactor.currentState.mapDetailInfo.mapUrl, !mapUrl.isEmpty { contentViews[0] = mapInfoView @@ -94,13 +90,13 @@ private extension MapDictionaryDetailViewController { mapInfoView.mapImageView.addGestureRecognizer(tapGesture) tapGesture.rx.event - .bind(onNext: { [weak self] _ in - guard let self else { return } - let viewController = PinchMapViewController(imageUrl: "") - viewController.modalPresentationStyle = .overFullScreen - self.present(viewController, animated: true) - }) - .disposed(by: disposeBag) + .bind(onNext: { [weak self] _ in + guard let self else { return } + let viewController = PinchMapViewController(imageUrl: "") + viewController.modalPresentationStyle = .overFullScreen + self.present(viewController, animated: true) + }) + .disposed(by: disposeBag) } } @@ -109,10 +105,15 @@ extension MapDictionaryDetailViewController { func bind(reactor: Reactor) { bindUserActions(reactor: reactor) bindViewState(reactor: reactor) - bindReportButton(providerId: reactor.state.map { $0.mapDetailInfo.mapId ?? 0}, itemName: reactor.state.map { $0.mapDetailInfo.nameKr ?? ""}) + bindReportButton(providerId: reactor.state.map { $0.mapDetailInfo.mapId ?? 0 }, itemName: reactor.state.map { $0.mapDetailInfo.nameKr ?? "" }) } private func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + appearMonsterView.filterButton.rx.tap .map { Reactor.Action.filterButtonTapped } .bind(to: reactor.action) @@ -123,48 +124,42 @@ extension MapDictionaryDetailViewController { reactor.state.map(\.mapDetailInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpMainInfo() - self?.setUpMapView() + .withUnretained(self) + .bind(onNext: { owner, item in + owner.setUpMainInfo() + owner.setUpMapView() + owner.mainView.setBookmark(isBookmarked: item.bookmarkId != nil) }) .disposed(by: disposeBag) + reactor.state.map(\.spawnMonsters) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpMonsterView() + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.setUpMonsterView() }) .disposed(by: disposeBag) + reactor.state.map(\.npcs) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpNpcView() - }) - .disposed(by: disposeBag) - - rx.viewDidAppear - .take(1) - .flatMapLatest { _ in return reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 .withUnretained(self) - .subscribe { (owner, route) in - switch route { - case .filter(let type): - let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: owner.selectedIndex) { index in - owner.selectedIndex = index - let selectedFilter = reactor.currentState.type.detailSortedFilter[index] - owner.appearMonsterView.selectFilter(selectedType: selectedFilter) - } - owner.tabBarController?.presentModal(viewController) - case .none: - break - } - } + .bind(onNext: { owner, _ in + owner.setUpNpcView() + }) .disposed(by: disposeBag) - rx.viewWillAppear.take(1).subscribe { _ in - reactor.action.onNext(.viewWillAppear) - } + bindBookmarkButton( + buttonTap: mainView.bookmarkButton.rx.tap, + currentItem: reactor.state.map { $0.mapDetailInfo }, + isLogin: { reactor.currentState.isLogin }, + imageUrl: { $0.mapUrl }, + backgroundColor: type.backgroundColor, + isBookmarked: { $0.bookmarkId != nil }, + toggleBookmark: { isDeleting in reactor.action.onNext(.toggleBookmark(isDeleting)) }, + undoLastDeleted: { reactor.action.onNext(.undoLastDeletedBookmark) } + ) .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift index c35f225d..4ebd3c2f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift @@ -3,134 +3,177 @@ import DomainInterface import ReactorKit public final class MonsterDictionaryDetailReactor: Reactor { - // MARK: - Reactor + // MARK: - Type public enum Route { case none case filter(DictionaryType) } - /// UI 구현을 위한 임시 모델(몬스터 상세정보) - struct TabMenu: Equatable { - var infos: [Info] - var maps: [DictionaryDetailMonsterMapResponse] - var items: [DictionaryDetailMonsterDropItemResponse] - - static func == (lhs: TabMenu, rhs: TabMenu) -> Bool { - return lhs.infos == rhs.infos && - lhs.maps == rhs.maps && - lhs.items == rhs.items - } - } public struct Info: Equatable { var name: String var desc: String } - // 임시 모델 - public struct Map: Equatable { - var mapName: String - var detailName: String - var maxSpawnCount: Int - var iconUrl: String - } - public struct Item: Equatable { - var itemName: String - var dropRate: Double - var imageUrl: String - var itemLevel: Int - } + // MARK: - Action public enum Action { case filterButtonTapped(DictionaryType) case viewWillAppear case selectFilter(SortType) + case toggleBookmark(Bool) + case undoLastDeletedBookmark } + // MARK: - Mutation public enum Mutation { case showFilter(DictionaryType) case setDetailData(DictionaryDetailMonsterResponse) case setDetailDropItemData([DictionaryDetailMonsterDropItemResponse]) case setDetailMapData([DictionaryDetailMonsterMapResponse]) + case setBookmark(DictionaryDetailMonsterResponse) + case setLastDeletedBookmark(DictionaryDetailMonsterResponse?) + case setLoginState(Bool) } + // MARK: - State public struct State { @Pulse var route: Route = .none - var type: DictionaryType = .item + var type: DictionaryType = .monster var id = 0 - var name = "슈미의 의뢰" - var level = 0 - var imageUrl = "" - var subTextLabel = "LV.21" - var tags: Effectiveness = Effectiveness(fire: nil, lightning: nil, poison: nil, holy: nil, ice: nil, physical: nil) - var menus = TabMenu( - infos: [], - maps: [], - items: [] + var monsterDetailInfo = DictionaryDetailMonsterResponse( + monsterId: 0, nameKr: "", nameEn: "", imageUrl: "", + level: 0, exp: 0, hp: 0, mp: 0, + physicalDefense: 0, magicDefense: 0, + requiredAccuracy: 0, bonusAccuracyPerLevelLower: 0, + evasionRate: 0, mesoDropAmount: nil, mesoDropRate: nil, + typeEffectiveness: nil, bookmarkId: nil ) + var dropItems = [DictionaryDetailMonsterDropItemResponse]() + var spawnMaps = [DictionaryDetailMonsterMapResponse]() + var infos = [Info]() + var isLogin = false + var lastDeletedBookmark: DictionaryDetailMonsterResponse? } - public let dictionaryDetailMonsterUseCase: FetchDictionaryDetailMonsterUseCase - public let dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase - public let dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase + // MARK: - Properties + private let disposeBag = DisposeBag() + private let dictionaryDetailMonsterUseCase: FetchDictionaryDetailMonsterUseCase + private let dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase + private let dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let setBookmarkUseCase: SetBookmarkUseCase public var initialState: State - private let disposBag = DisposeBag() - public init(dictionaryDetailMonsterUseCase: FetchDictionaryDetailMonsterUseCase, dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase, dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase, id: Int) { + // MARK: - Init + public init( + dictionaryDetailMonsterUseCase: FetchDictionaryDetailMonsterUseCase, + dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase, + dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase, + checkLoginUseCase: CheckLoginUseCase, + setBookmarkUseCase: SetBookmarkUseCase, + id: Int + ) { self.initialState = State(type: .monster, id: id) self.dictionaryDetailMonsterUseCase = dictionaryDetailMonsterUseCase self.dictionaryDetailMonsterDropItemUseCase = dictionaryDetailMonsterDropItemUseCase self.dictionaryDetailMonsterMapUseCase = dictionaryDetailMonsterMapUseCase + self.checkLoginUseCase = checkLoginUseCase + self.setBookmarkUseCase = setBookmarkUseCase } + // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { case let .filterButtonTapped(type): - return Observable.just(.showFilter(type)) + return .just(.showFilter(type)) + case .viewWillAppear: - return .concat([ - dictionaryDetailMonsterUseCase.execute(id: currentState.id).map {.setDetailData($0)}, - dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: nil).map {.setDetailDropItemData($0)}, - dictionaryDetailMonsterMapUseCase.execute(id: currentState.id).map {.setDetailMapData($0)} + return .merge([ + checkLoginUseCase.execute().map { .setLoginState($0) }, + dictionaryDetailMonsterUseCase.execute(id: currentState.id).map { .setDetailData($0) }, + dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: nil).map { .setDetailDropItemData($0) }, + dictionaryDetailMonsterMapUseCase.execute(id: currentState.id).map { .setDetailMapData($0) } ]) + case let .selectFilter(type): switch type { case .levelDESC: // 레벨 높은 순 - return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: ["level", "desc"]).map {.setDetailDropItemData($0)} + return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: ["level", "desc"]).map { .setDetailDropItemData($0) } case .levelASC: // 레벨 낮은 순 - return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: ["level,asc"]).map {.setDetailDropItemData($0)} + return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: ["level", "asc"]).map { .setDetailDropItemData($0) } case .mostDrop: - return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: nil).map {.setDetailDropItemData($0)} + return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: nil).map { .setDetailDropItemData($0) } default: - return Observable.empty() + return .empty() } + + case let .toggleBookmark(isSelected): + let monsterId = currentState.monsterDetailInfo.monsterId + + let saveDeleted: Observable = isSelected + ? .just(.setLastDeletedBookmark(currentState.monsterDetailInfo)) + : .just(.setLastDeletedBookmark(nil)) + + return saveDeleted.concat( + setBookmarkUseCase.execute( + bookmarkId: currentState.monsterDetailInfo.bookmarkId ?? monsterId, + isBookmark: isSelected ? .delete : .set(.monster) + ) + .andThen(dictionaryDetailMonsterUseCase.execute(id: currentState.id).map { .setDetailData($0) }) + ) + + case .undoLastDeletedBookmark: + guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } + let monsterId = lastDeleted.monsterId + + return setBookmarkUseCase.execute( + bookmarkId: monsterId, + isBookmark: .set(.monster) + ) + .andThen( + Observable.concat([ + dictionaryDetailMonsterUseCase.execute(id: currentState.id).map { .setDetailData($0) }, + .just(.setLastDeletedBookmark(nil)) + ]) + ) } } + // MARK: - Reduce public func reduce(state: State, mutation: Mutation) -> State { var newState = state + switch mutation { case let .showFilter(type): - newState.type = type newState.route = .filter(type) + case let .setDetailData(data): - newState.name = data.nameKr - newState.level = data.level + newState.monsterDetailInfo = data + var infos: [Info] = [] infos.append(.init(name: "HP", desc: "\(data.hp)")) infos.append(.init(name: "MP", desc: "\(data.mp)")) infos.append(.init(name: "EXP", desc: "\(data.exp)")) infos.append(.init(name: "물리방어력", desc: "\(data.physicalDefense)")) infos.append(.init(name: "마법방어력", desc: "\(data.magicDefense)")) - if let typeEffectiveness = data.typeEffectiveness { - newState.tags = typeEffectiveness - } - newState.imageUrl = data.imageUrl - newState.menus.infos = infos + newState.infos = infos + case let .setDetailDropItemData(data): - newState.menus.items = data + newState.dropItems = data + case let .setDetailMapData(data): - newState.menus.maps = data + newState.spawnMaps = data + + case let .setBookmark(monster): + newState.monsterDetailInfo = monster + + case let .setLastDeletedBookmark(monster): + newState.lastDeletedBookmark = monster + + case let .setLoginState(isLogin): + newState.isLogin = isLogin } + return newState } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift index c6258d41..4c655911 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift @@ -20,7 +20,6 @@ class MonsterDictionaryDetailViewController: DictionaryDetailBaseViewController, private var appearMapView = DetailStackCardView() private var dropItemView = DetailStackCardView() private let sortedFactory: SortedBottomSheetFactory = SortedBottomSheetFactoryImpl() - } // MARK: - Populate Data @@ -40,7 +39,7 @@ private extension MonsterDictionaryDetailViewController { func setUpInfoStackView() { guard let reactor = reactor else { return } - let infos = reactor.currentState.menus.infos + let infos = reactor.currentState.infos contentViews.append(detailView) @@ -54,7 +53,7 @@ private extension MonsterDictionaryDetailViewController { let filter = reactor.currentState.type.detailSortedFilter.first else { return } appearMapView.initFilter(firstFilter: filter) - let maps = reactor.currentState.menus.maps + let maps = reactor.currentState.spawnMaps contentViews.append(appearMapView) if maps.isEmpty { contentViews[1] = DetailEmptyView(type: .appearMap) @@ -77,7 +76,7 @@ private extension MonsterDictionaryDetailViewController { func setUpDropItemView() { guard let reactor = reactor else { return } - let items = reactor.currentState.menus.items + let items = reactor.currentState.dropItems dropItemView.reset() contentViews.append(dropItemView) // 드롭아이템 @@ -105,18 +104,23 @@ private extension MonsterDictionaryDetailViewController { // MARK: - Bind extension MonsterDictionaryDetailViewController { - public func bind(reactor: Reactor) { bindcUserActions(reactor: reactor) bindViewState(reactor: reactor) - bindReportButton(providerId: reactor.state.map { $0.id }, itemName: reactor.state.map { $0.name }) + bindReportButton(providerId: reactor.state.map { $0.id }, itemName: reactor.state.map { $0.monsterDetailInfo.nameKr }) } private func bindcUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + dropItemView.filterButton.rx.tap .map { Reactor.Action.filterButtonTapped(.item) } .bind(to: reactor.action) .disposed(by: disposeBag) + appearMapView.filterButton.rx.tap .map { Reactor.Action.filterButtonTapped(.map) } .bind(to: reactor.action) @@ -129,40 +133,35 @@ extension MonsterDictionaryDetailViewController { isBottomTabbarHidden = true - reactor.state.map(\.tags) + reactor.state.map(\.monsterDetailInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: { [weak self] tags in - self?.makeTagsRow(tags) + .withUnretained(self) + .bind(onNext: { owner, monster in + if let tags = monster.typeEffectiveness { + owner.makeTagsRow(tags) + } + owner.setUpMainInfo(name: reactor.currentState.monsterDetailInfo.nameKr, subText: "Lv. \(reactor.currentState.monsterDetailInfo.level)", imageUrl: reactor.currentState.monsterDetailInfo.imageUrl) + owner.mainView.setBookmark(isBookmarked: monster.bookmarkId != nil) + owner.setUpInfoStackView() }) .disposed(by: disposeBag) - reactor.state.map(\.name) - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpMainInfo(name: reactor.currentState.name, subText: "\(reactor.currentState.level)", imageUrl: reactor.currentState.imageUrl) - }) - .disposed(by: disposeBag) - reactor.state.map(\.menus.infos) + reactor.state.map(\.spawnMaps) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpInfoStackView() - }) - .disposed(by: disposeBag) - reactor.state.map(\.menus.maps) - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpMapView() + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.setUpMapView() }) .disposed(by: disposeBag) - reactor.state.map(\.menus.items) + + reactor.state.map(\.dropItems) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpDropItemView() + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.setUpDropItemView() }) .disposed(by: disposeBag) @@ -196,12 +195,16 @@ extension MonsterDictionaryDetailViewController { } .disposed(by: disposeBag) - rx.viewWillAppear - .take(1) - .subscribe { _ in - // TODO: 디테일 API 호출 - reactor.action.onNext(.viewWillAppear) - } - .disposed(by: disposeBag) + bindBookmarkButton( + buttonTap: mainView.bookmarkButton.rx.tap, + currentItem: reactor.state.map { $0.monsterDetailInfo }, + isLogin: { reactor.currentState.isLogin }, + imageUrl: { $0.imageUrl }, + backgroundColor: type.backgroundColor, + isBookmarked: { $0.bookmarkId != nil }, + toggleBookmark: { isDeleting in reactor.action.onNext(.toggleBookmark(isDeleting)) }, + undoLastDeleted: { reactor.action.onNext(.undoLastDeletedBookmark) } + ) + .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift index ef0f9b92..43a85137 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift @@ -3,91 +3,151 @@ import DomainInterface import ReactorKit public final class NpcDictionaryDetailReactor: Reactor { - // MARK: - Reactor + // MARK: - Route public enum Route { case none case filter(DictionaryType) } + // MARK: - Action public enum Action { case filterButtonTapped case viewWillAppear case selectFilter(SortType) + case toggleBookmark(Bool) + case undoLastDeletedBookmark } + // MARK: - Mutation public enum Mutation { case showFilter case setDetailData(DictionaryDetailNpcResponse) case setDetailMaps([DictionaryDetailMonsterMapResponse]) case setDetailQuests([DictionaryDetailNpcQuestResponse]) - } - public struct Info: Equatable { - var name: String - var imgUrl: String? - var subText: String + case setLoginState(Bool) + case setLastDeletedBookmark(DictionaryDetailNpcResponse?) } + // MARK: - State public struct State { @Pulse var route: Route = .none - var info = Info(name: "", subText: "") - var isBookmarked: Bool = false + var npcDetailInfo: DictionaryDetailNpcResponse var type: DictionaryType = .npc var maps: [DictionaryDetailMonsterMapResponse] var quests: [DictionaryDetailNpcQuestResponse] - var id = 0 + var id: Int + var isLogin = false + var lastDeletedBookmark: DictionaryDetailNpcResponse? } - public let dictionaryDetailNpcUseCase: FetchDictionaryDetailNpcUseCase - public let dictionaryDetailNpcQuestUseCase: FetchDictionaryDetailNpcQuestUseCase - public let dictionaryDetailNpcMapUseCase: FetchDictionaryDetailNpcMapUseCase + // MARK: - UseCases + private let dictionaryDetailNpcUseCase: FetchDictionaryDetailNpcUseCase + private let dictionaryDetailNpcQuestUseCase: FetchDictionaryDetailNpcQuestUseCase + private let dictionaryDetailNpcMapUseCase: FetchDictionaryDetailNpcMapUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let setBookmarkUseCase: SetBookmarkUseCase public var initialState: State - private let disposBag = DisposeBag() - - public init(dictionaryDetailNpcUseCase: FetchDictionaryDetailNpcUseCase, dictionaryDetailNpcQuestUseCase: FetchDictionaryDetailNpcQuestUseCase, dictionaryDetailNpcMapUseCase: FetchDictionaryDetailNpcMapUseCase, id: Int) { + private let disposeBag = DisposeBag() - initialState = State(type: .npc, maps: [], quests: [], id: id) + // MARK: - Init + public init( + dictionaryDetailNpcUseCase: FetchDictionaryDetailNpcUseCase, + dictionaryDetailNpcQuestUseCase: FetchDictionaryDetailNpcQuestUseCase, + dictionaryDetailNpcMapUseCase: FetchDictionaryDetailNpcMapUseCase, + checkLoginUseCase: CheckLoginUseCase, + setBookmarkUseCase: SetBookmarkUseCase, + id: Int + ) { self.dictionaryDetailNpcUseCase = dictionaryDetailNpcUseCase self.dictionaryDetailNpcQuestUseCase = dictionaryDetailNpcQuestUseCase self.dictionaryDetailNpcMapUseCase = dictionaryDetailNpcMapUseCase + self.checkLoginUseCase = checkLoginUseCase + self.setBookmarkUseCase = setBookmarkUseCase + self.initialState = State( + npcDetailInfo: DictionaryDetailNpcResponse( + npcId: 0, nameKr: "", nameEn: "", iconUrlDetail: nil, bookmarkId: nil + ), + maps: [], + quests: [], + id: id + ) } + // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { case .filterButtonTapped: - return Observable.just(.showFilter) + return .just(.showFilter) case .viewWillAppear: - return .concat([ - dictionaryDetailNpcUseCase.execute(id: currentState.id).map {.setDetailData($0)}, - dictionaryDetailNpcMapUseCase.execute(id: currentState.id).map {.setDetailMaps($0)}, - dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: nil).map {.setDetailQuests($0)} + return .merge([ + checkLoginUseCase.execute().map { .setLoginState($0) }, + dictionaryDetailNpcUseCase.execute(id: currentState.id).map { .setDetailData($0) }, + dictionaryDetailNpcMapUseCase.execute(id: currentState.id).map { .setDetailMaps($0) }, + dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: nil).map { .setDetailQuests($0) } ]) case let .selectFilter(type): switch type { case .levelHighest: - return dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: ["maxLevel", "desc"]).map {.setDetailQuests($0)} + return dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: ["maxLevel", "desc"]).map { .setDetailQuests($0) } case .levelLowest: - return dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: ["minLevel", "asc"]).map {.setDetailQuests($0)} + return dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: ["minLevel", "asc"]).map { .setDetailQuests($0) } default: return .empty() } + + case let .toggleBookmark(isSelected): + let npcId = currentState.npcDetailInfo.npcId + + let saveDeleted: Observable = isSelected + ? .just(.setLastDeletedBookmark(currentState.npcDetailInfo)) + : .just(.setLastDeletedBookmark(nil)) + + return saveDeleted.concat( + setBookmarkUseCase.execute( + bookmarkId: currentState.npcDetailInfo.bookmarkId ?? npcId, + isBookmark: isSelected ? .delete : .set(.npc) + ) + .andThen( + dictionaryDetailNpcUseCase.execute(id: currentState.id) + .map { .setDetailData($0) } + ) + ) + + case .undoLastDeletedBookmark: + guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } + let npcId = lastDeleted.npcId + + return setBookmarkUseCase.execute( + bookmarkId: npcId, + isBookmark: .set(.npc) + ) + .andThen( + Observable.concat([ + dictionaryDetailNpcUseCase.execute(id: currentState.id) + .map { .setDetailData($0) }, + .just(.setLastDeletedBookmark(nil)) + ]) + ) } } + // MARK: - Reduce public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { case .showFilter: newState.route = .filter(newState.type) case let .setDetailData(data): - newState.info.name = data.nameKr - newState.info.subText = data.nameEn - newState.info.imgUrl = data.iconUrlDetail - newState.isBookmarked = data.bookmarkId != nil + newState.npcDetailInfo = data case let .setDetailMaps(data): newState.maps = data case let .setDetailQuests(data): newState.quests = data + case let .setLoginState(isLogin): + newState.isLogin = isLogin + case let .setLastDeletedBookmark(map): + newState.lastDeletedBookmark = map } return newState } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift index 57ef9fd7..2b3a8619 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift @@ -15,10 +15,6 @@ final class NpcDictionaryDetailViewController: DictionaryDetailBaseViewControlle private var appearMapView = DetailStackCardView() private var questView = DetailStackCardView() private let sortedFactory: SortedBottomSheetFactory = SortedBottomSheetFactoryImpl() - - init(imageUrl: String) { - super.init(type: .npc) - } } // MARK: - SetUp @@ -26,7 +22,7 @@ private extension NpcDictionaryDetailViewController { // 매개변수로 넘겨주는 것과 func setUpMainInfo(name: String, imageUrl: String?) { // 상세정보(메인) - self.inject( + inject( input: DictionaryDetailBaseViewController .Input( imageUrl: imageUrl, @@ -36,6 +32,7 @@ private extension NpcDictionaryDetailViewController { ) ) } + // 내부에서 리액터 사용해서 하는 것 func setUpMapView() { guard let reactor = reactor else { return } @@ -52,10 +49,10 @@ private extension NpcDictionaryDetailViewController { type: .appearMap, imageUrl: map.iconUrl, mainText: map.mapName, - subText: map.detailName)) + subText: map.detailName + )) } } - } func setUpQuestView() { @@ -83,10 +80,15 @@ extension NpcDictionaryDetailViewController { func bind(reactor: Reactor) { bindUserActions(reactor: reactor) bindViewState(reactor: reactor) - bindReportButton(providerId: reactor.state.map { $0.id }, itemName: reactor.state.map { $0.info.name }) + bindReportButton(providerId: reactor.state.map { $0.id }, itemName: reactor.state.map { $0.npcDetailInfo.nameKr }) } private func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + questView.filterButton.rx.tap .map { Reactor.Action.filterButtonTapped } .bind(to: reactor.action) @@ -100,9 +102,9 @@ extension NpcDictionaryDetailViewController { rx.viewDidAppear .take(1) - .flatMapLatest { _ in return reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 + .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 .withUnretained(self) - .subscribe { (owner, route) in + .subscribe { owner, route in switch route { case .filter(let type): let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: owner.selectedIndex) { index in @@ -117,34 +119,45 @@ extension NpcDictionaryDetailViewController { } } .disposed(by: disposeBag) - reactor.state.map(\.info) + + reactor.state.map(\.npcDetailInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] info in - self?.setUpMainInfo(name: info.name, imageUrl: info.imgUrl) + .withUnretained(self) + .bind(onNext: { owner, map in + owner.setUpMainInfo(name: map.nameKr, imageUrl: map.iconUrlDetail) + owner.mainView.setBookmark(isBookmarked: map.bookmarkId != nil) }) .disposed(by: disposeBag) + reactor.state.map(\.maps) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpMapView() + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.setUpMapView() }) .disposed(by: disposeBag) reactor.state.map(\.quests) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpQuestView() + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.setUpQuestView() }) .disposed(by: disposeBag) - rx.viewWillAppear - .take(1) - .subscribe { _ in - reactor.action.onNext(.viewWillAppear) - } - .disposed(by: disposeBag) + bindBookmarkButton( + buttonTap: mainView.bookmarkButton.rx.tap, + currentItem: reactor.state.map { $0.npcDetailInfo }, + isLogin: { reactor.currentState.isLogin }, + imageUrl: { $0.iconUrlDetail }, + backgroundColor: type.backgroundColor, + isBookmarked: { $0.bookmarkId != nil }, + toggleBookmark: { isDeleting in reactor.action.onNext(.toggleBookmark(isDeleting)) }, + undoLastDeleted: { reactor.action.onNext(.undoLastDeletedBookmark) } + ) + .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift index a0b325d7..ae407c66 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift @@ -2,47 +2,117 @@ import DomainInterface import ReactorKit -final class QuestDictionaryDetailReactor: Reactor { - - public struct State { - var type: DictionaryItemType - - var detailInfo: DictionaryDetailQuestResponse - var linkedQuestInfo: DictionaryDetailQuestLinkedQuestsResponse - - var id = 0 - } - - public let dictionaryDetailQuestUseCase: FetchDictionaryDetailQuestUseCase - public let dictionaryDetailQuestLinkedQuestUseCase: FetchDictionaryDetailQuestLinkedQuestsUseCase - +public final class QuestDictionaryDetailReactor: Reactor { + // MARK: - Action / Mutation / State public enum Action { case viewWillAppear + case toggleBookmark(Bool) + case undoLastDeletedBookmark } public enum Mutation { case setDetailData(DictionaryDetailQuestResponse) case setLinkedQuests(DictionaryDetailQuestLinkedQuestsResponse) + case setLoginState(Bool) + case setLastDeletedBookmark(DictionaryDetailQuestResponse?) } + public struct State { + var type: DictionaryType = .quest + var id: Int + var detailInfo: DictionaryDetailQuestResponse + var linkedQuestInfo: DictionaryDetailQuestLinkedQuestsResponse + var isLogin = false + var lastDeletedBookmark: DictionaryDetailQuestResponse? + } + + private let dictionaryDetailQuestUseCase: FetchDictionaryDetailQuestUseCase + private let dictionaryDetailQuestLinkedQuestUseCase: FetchDictionaryDetailQuestLinkedQuestsUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let setBookmarkUseCase: SetBookmarkUseCase + public var initialState: State private let disposeBag = DisposeBag() - public init(dictionaryDetailQuestUseCase: FetchDictionaryDetailQuestUseCase, dictionaryDetailQuestLinkedQuestsUseCase: FetchDictionaryDetailQuestLinkedQuestsUseCase, id: Int) { - self.initialState = .init(type: .quest, detailInfo: DictionaryDetailQuestResponse(questId: nil, titlePrefix: nil, nameKr: nil, nameEn: nil, iconUrl: nil, questType: nil, minLevel: nil, maxLevel: nil, requiredMesoStart: nil, startNpcId: nil, startNpcName: nil, endNpcId: nil, endNpcName: nil, reward: nil, rewardItems: nil, requirements: nil, allowedJobs: nil, bookmarkId: nil), linkedQuestInfo: DictionaryDetailQuestLinkedQuestsResponse(previousQuests: nil, nextQuests: nil), id: id) + public init( + dictionaryDetailQuestUseCase: FetchDictionaryDetailQuestUseCase, + dictionaryDetailQuestLinkedQuestUseCase: FetchDictionaryDetailQuestLinkedQuestsUseCase, + checkLoginUseCase: CheckLoginUseCase, + setBookmarkUseCase: SetBookmarkUseCase, + id: Int + ) { self.dictionaryDetailQuestUseCase = dictionaryDetailQuestUseCase - self.dictionaryDetailQuestLinkedQuestUseCase = dictionaryDetailQuestLinkedQuestsUseCase + self.dictionaryDetailQuestLinkedQuestUseCase = dictionaryDetailQuestLinkedQuestUseCase + self.checkLoginUseCase = checkLoginUseCase + self.setBookmarkUseCase = setBookmarkUseCase + self.initialState = .init( + id: id, + detailInfo: .init( + questId: nil, + titlePrefix: nil, + nameKr: nil, + nameEn: nil, + iconUrl: nil, + questType: nil, + minLevel: nil, + maxLevel: nil, + requiredMesoStart: nil, + startNpcId: nil, + startNpcName: nil, + endNpcId: nil, + endNpcName: nil, + reward: nil, + rewardItems: nil, + requirements: nil, + allowedJobs: nil, + bookmarkId: nil + ), + linkedQuestInfo: .init(previousQuests: nil, nextQuests: nil) + ) } public func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: - return .concat([ - dictionaryDetailQuestUseCase.execute(id: currentState.id).map {.setDetailData($0)}, - dictionaryDetailQuestLinkedQuestUseCase.execute(id: currentState.id).map {.setLinkedQuests( $0 ) - } - + return .merge([ + checkLoginUseCase.execute().map { .setLoginState($0) }, + dictionaryDetailQuestUseCase.execute(id: currentState.id).map { .setDetailData($0) }, + dictionaryDetailQuestLinkedQuestUseCase.execute(id: currentState.id).map { .setLinkedQuests($0) } ]) + + case let .toggleBookmark(isSelected): + guard let questId = currentState.detailInfo.questId else { return .empty() } + + let saveDeleted: Observable = isSelected + ? .just(.setLastDeletedBookmark(currentState.detailInfo)) + : .just(.setLastDeletedBookmark(nil)) + + return saveDeleted.concat( + setBookmarkUseCase.execute( + bookmarkId: currentState.detailInfo.bookmarkId ?? questId, + isBookmark: isSelected ? .delete : .set(.quest) + ) + .andThen( + dictionaryDetailQuestUseCase.execute(id: currentState.id) + .map { .setDetailData($0) } + ) + ) + + case .undoLastDeletedBookmark: + guard let lastDeleted = currentState.lastDeletedBookmark, + let questId = lastDeleted.questId else { return .empty() } + + return setBookmarkUseCase.execute( + bookmarkId: questId, + isBookmark: .set(.quest) + ) + .andThen( + Observable.concat([ + dictionaryDetailQuestUseCase.execute(id: currentState.id) + .map { .setDetailData($0) }, + .just(.setLastDeletedBookmark(nil)) + ]) + ) } } @@ -53,6 +123,10 @@ final class QuestDictionaryDetailReactor: Reactor { newState.detailInfo = data case let .setLinkedQuests(data): newState.linkedQuestInfo = data + case let .setLoginState(isLogin): + newState.isLogin = isLogin + case let .setLastDeletedBookmark(data): + newState.lastDeletedBookmark = data } return newState } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift index 00d45137..195c9b83 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift @@ -4,6 +4,8 @@ import DesignSystem import DomainInterface import ReactorKit +import RxCocoa +import RxSwift final class QuestDictionaryDetailViewController: DictionaryDetailBaseViewController, View { public typealias Reactor = QuestDictionaryDetailReactor @@ -16,11 +18,11 @@ final class QuestDictionaryDetailViewController: DictionaryDetailBaseViewControl private extension QuestDictionaryDetailViewController { func setUpMainInfo() { // 상세 정보(메인?) - self.inject(input: DictionaryDetailBaseViewController.Input( + inject(input: DictionaryDetailBaseViewController.Input( imageUrl: reactor?.currentState.detailInfo.iconUrl, backgroundColor: type.backgroundColor, name: reactor?.currentState.detailInfo.nameKr ?? "이름 없음", - subText: "Lv.\(reactor?.currentState.detailInfo.minLevel ?? 0)" + subText: "수락Lv.\(reactor?.currentState.detailInfo.minLevel ?? 0)" )) } @@ -99,7 +101,7 @@ private extension QuestDictionaryDetailViewController { let quests = reactor.currentState.linkedQuestInfo contentViews.append(linkedQuestView) if let previousQuests = quests.previousQuests, let nextQuests = quests.nextQuests { - if previousQuests.isEmpty && nextQuests.isEmpty { + if previousQuests.isEmpty, nextQuests.isEmpty { contentViews[1] = DetailEmptyView(type: .quest) } else { contentViews[1] = linkedQuestView @@ -109,7 +111,6 @@ private extension QuestDictionaryDetailViewController { } } } - } } @@ -118,32 +119,47 @@ extension QuestDictionaryDetailViewController { public func bind(reactor: Reactor) { bindUserAction(reactor: reactor) bindViewState(reactor: reactor) - bindReportButton(providerId: reactor.state.map { $0.detailInfo.questId ?? 0 }, itemName: reactor.state.map { $0.detailInfo.nameKr ?? ""}) + bindReportButton(providerId: reactor.state.map { $0.detailInfo.questId ?? 0 }, itemName: reactor.state.map { $0.detailInfo.nameKr ?? "" }) } - private func bindUserAction(reactor: Reactor) {} + private func bindUserAction(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } private func bindViewState(reactor: Reactor) { reactor.state.map(\.detailInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpMainInfo() - self?.setUpInfoStackView() + .withUnretained(self) + .bind(onNext: { owner, map in + owner.setUpMainInfo() + owner.setUpInfoStackView() + owner.mainView.setBookmark(isBookmarked: map.bookmarkId != nil) }) .disposed(by: disposeBag) reactor.state.map(\.linkedQuestInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: {[weak self] _ in - self?.setUpQuestView() + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.setUpQuestView() }) .disposed(by: disposeBag) - rx.viewWillAppear.take(1).subscribe { _ in - reactor.action.onNext(.viewWillAppear) - } + bindBookmarkButton( + buttonTap: mainView.bookmarkButton.rx.tap, + currentItem: reactor.state.map { $0.detailInfo }, + isLogin: { reactor.currentState.isLogin }, + imageUrl: { $0.iconUrl }, + backgroundColor: type.backgroundColor, + isBookmarked: { $0.bookmarkId != nil }, + toggleBookmark: { isDeleting in reactor.action.onNext(.toggleBookmark(isDeleting)) }, + undoLastDeleted: { reactor.action.onNext(.undoLastDeletedBookmark) } + ) .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackMapView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackMapView.swift index 97ca7041..380efa01 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackMapView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackMapView.swift @@ -49,8 +49,10 @@ private extension DetailStackMapView { isLayoutMarginsRelativeArrangement = true layoutMargins = Constant.mapLayoutMargin } +} - func setUpMapView(imageUrl: String) { +extension DetailStackMapView { + func setUpMapView(imageUrl: String?) { ImageLoader.shared.loadImage(stringURL: imageUrl) { [weak self] image in self?.mapImageView.image = image } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListFactoryImpl.swift index 85810c73..47a37655 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListFactoryImpl.swift @@ -1,3 +1,4 @@ +import AuthFeatureInterface import BaseFeature import BookmarkFeatureInterface import DictionaryFeatureInterface @@ -19,6 +20,7 @@ public final class DictionaryListFactoryImpl: DictionaryMainListFactory { private let sortedFactory: SortedBottomSheetFactory private let bookmarkModalFactory: BookmarkModalFactory private let detailFactory: DictionaryDetailFactory + private let loginFactory: () -> LoginFactory public init( checkLoginUseCase: CheckLoginUseCase, @@ -34,7 +36,8 @@ public final class DictionaryListFactoryImpl: DictionaryMainListFactory { monsterFilterFactory: MonsterFilterBottomSheetFactory, sortedFactory: SortedBottomSheetFactory, bookmarkModalFactory: BookmarkModalFactory, - detailFactory: DictionaryDetailFactory + detailFactory: DictionaryDetailFactory, + loginFactory: @escaping () -> LoginFactory ) { self.checkLoginUseCase = checkLoginUseCase self.dictionaryAllListItemUseCase = dictionaryAllListItemUseCase @@ -50,6 +53,7 @@ public final class DictionaryListFactoryImpl: DictionaryMainListFactory { self.sortedFactory = sortedFactory self.bookmarkModalFactory = bookmarkModalFactory self.detailFactory = detailFactory + self.loginFactory = loginFactory } public func make(type: DictionaryType, listType: DictionaryMainViewType, keyword: String? = "") -> BaseViewController { @@ -66,7 +70,7 @@ public final class DictionaryListFactoryImpl: DictionaryMainListFactory { setBookmarkUseCase: setBookmarkUseCase, parseItemFilterResultUseCase: parseItemFilterResultUseCase ) - let viewController = DictionaryListViewController(reactor: reactor, itemFilterFactory: itemFilterFactory, monsterFilterFactory: monsterFilterFactory, sortedFactory: sortedFactory, bookmarkModalFactory: bookmarkModalFactory, detailFactory: detailFactory) + let viewController = DictionaryListViewController(reactor: reactor, itemFilterFactory: itemFilterFactory, monsterFilterFactory: monsterFilterFactory, sortedFactory: sortedFactory, bookmarkModalFactory: bookmarkModalFactory, detailFactory: detailFactory, loginFactory: loginFactory()) if listType == .search { viewController.isBottomTabbarHidden = true } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift index 4fa5f65b..abc6b8de 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift @@ -1,5 +1,6 @@ -import DomainInterface +// swiftlint:disable function_body_length +import DomainInterface import ReactorKit import RxSwift @@ -14,15 +15,15 @@ open class DictionaryListReactor: Reactor { // MARK: - Action public enum Action { case toggleBookmark(Int, Bool) - case viewDidLoad + case viewWillAppear case sortButtonTapped case filterButtonTapped - case sortOptionSelected(SortType) // 정렬 옵션 선택 시 액션 - case filterOptionSelected(startLevel: Int, endLevel: Int) // 필터 옵션 선택 시 액션 + case sortOptionSelected(SortType) + case filterOptionSelected(startLevel: Int, endLevel: Int) case itemFilterOptionSelected([(String, String)]) case setCurrentPage - case fetchList // data 불러오기 - case fetchListFilter // 필터링된 data 불러오기 + case fetchList + case fetchListFilter case undoLastDeletedBookmark } @@ -39,7 +40,7 @@ open class DictionaryListReactor: Reactor { case initPage case setLoginState(Bool) case setLastDeletedBookmark(DictionaryMainItemResponse?) - case setJobId([Int]) // jobId 설정 + case setJobId([Int]) case setCategoryId([Int]) } @@ -49,14 +50,13 @@ open class DictionaryListReactor: Reactor { public var listItems: [DictionaryMainItemResponse] = [] public var type: DictionaryType - // 필터 조건 public var keyword: String? public var jobId: [Int]? public var minLevel: Int? public var maxLevel: Int? public var categoryIds: [Int]? public var sort: String? - public var startLevel: Int? = 0 + public var startLevel: Int? = 1 public var endLevel: Int? = 200 public var currentPage = 0 @@ -110,233 +110,141 @@ open class DictionaryListReactor: Reactor { // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { - case .viewDidLoad: - // 로그인 체크 + 초기 데이터 fetch - return checkLoginUseCase.execute() - .flatMap { [weak self] _ -> Observable in - guard let self = self else { return .empty() } - -// if !isLoggedIn { -// // 로그인 안 되어 있으면 상태만 업데이트 -// return .just(.setLoginState(false)) -// } - - // 로그인 되어 있으면 상태 업데이트 후 초기 데이터 fetch - let loginMutation: Observable = .just(.setLoginState(true)) - let fetchMutation: Observable - - // monster: keyword, minLevel, maxLevel, page, size, sort - // npc: keyword, page, size, sort - // quest: keyword, page, size, sort - // item: keyword, jobId, minLevel, maxLevel, categoryIds, page, size, sort - // 몬스터, 아이템을 제외하고는 피그마 상으로는 정렬이 불가능한데, API에는 정렬 옵션이 있음. -> npc, quest,map 도 일단 정렬 옵션 추가 - switch self.currentState.type { - case .monster: - fetchMutation = Observable.concat([ - .just(.initPage), // 먼저 페이지 초기화 - self.dictionaryListUseCase - .execute( - type: .monster, - query: DictionaryListQuery(keyword: self.currentState.keyword ?? "", page: 0, size: 20, sort: nil) - ) - .map { Mutation.setListItem($0) } - ]) - case .npc: - fetchMutation = Observable.concat([ - .just(.initPage), - self.dictionaryNpcListUseCase - .execute(keyword: self.currentState.keyword ?? "", page: 0, size: 20, sort: nil) - .map { Mutation.setListItem($0) } - ]) - case .quest: - fetchMutation = Observable.concat([ - .just(.initPage), - self.dictionaryQuestListUseCase - .execute(keyword: self.currentState.keyword ?? "", page: 0, size: 20, sort: nil) - .map { Mutation.setListItem($0) } - ]) - case .item: - fetchMutation = Observable.concat([ - .just(.initPage), - self.dictionaryItemListUseCase - .execute(keyword: self.currentState.keyword ?? "", jobId: nil, minLevel: nil, maxLevel: nil, categoryIds: nil, page: 0, size: 20, sort: nil) - .map { Mutation.setListItem($0) } - ]) - case .map: - fetchMutation = Observable.concat([ - .just(.initPage), - self.dictionaryMapListUseCase - .execute(keyword: self.currentState.keyword ?? "", page: 0, size: 20, sort: nil) - .map { Mutation.setListItem($0) } - ]) - case .total: - fetchMutation = Observable.concat([ - .just(.initPage), - self.dictionaryAllListUseCase.execute(keyword: self.currentState.keyword ?? "", page: 0).map {Mutation.setListItem($0)} - ]) - default: - fetchMutation = .empty() - } - - return Observable.concat([loginMutation, fetchMutation]) - } + case .viewWillAppear: + return handleViewWillAppear() case let .toggleBookmark(id, isSelected): - guard let type = currentState.type.toItemType, - let bookmarkItem = currentState.listItems.first(where: { $0.id == id }) else { return .empty() } - let bookmarkId = bookmarkItem.bookmarkId ?? 0 - - let saveDeletedMutation: Observable = isSelected ? .just(.setLastDeletedBookmark(bookmarkItem)) : .just(.setLastDeletedBookmark(nil)) - - return saveDeletedMutation - .concat( - setBookmarkUseCase.execute( - bookmarkId: isSelected ? bookmarkId : id, - isBookmark: isSelected ? .delete : .set(type) - ) - .andThen( - Observable.concat([ - .just(.initPage), - fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) - ]) - ) - ) + return handleToggleBookmark(id: id, isSelected: isSelected) case .sortButtonTapped: return .just(.showSortFilter) + case .filterButtonTapped: return .just(.showFilter) + case let .sortOptionSelected(sort): - return .concat([ - .just(.setSort(sort.sortParameter)), - .just(.initPage) - ]) + return handleSortOptionSelected(sort: sort) + case let .filterOptionSelected(startLevel, endLevel): - // 필터 선택 후 페이지 초기화 - return .concat([ - .just(.setFilter(start: startLevel, end: endLevel)), - .just(.initPage) - ]) - .concat(Observable.deferred { [weak self] in - // 상태 적용 이후 호출 - guard let self = self else { return .empty() } - return self.fetchListFilter(sort: self.currentState.sort, startLevel: startLevel, endLevel: endLevel) - }) + return handleFilterOptionSelected(startLevel: startLevel, endLevel: endLevel) + case .setCurrentPage: - /* 기존 구조의 문제점 - 현재 상태에서 currentPage == 0 - .setCurrentPage 전달됨 -> 나중에 reduce에서 currentPage += 1 적용 됨 - 하지만 이 시점에 fetchList는 아직 currentPage == 0인 상태로 실행. - 개선필요. -> fetchList 액션 추가 - setCurrentPage 후에 fetchList 호출 함으로써 페이지 올리고, 데이터 불러오기 - */ return .just(.setCurrentPage) + case .fetchList: - return fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) - case .fetchListFilter: - return fetchListFilter(sort: currentState.sort, startLevel: currentState.startLevel ?? 1, endLevel: currentState.endLevel ?? 200) - case .undoLastDeletedBookmark: - guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } - return setBookmarkUseCase.execute( - bookmarkId: lastDeleted.id, - isBookmark: .set(lastDeleted.type) + return fetchList( + sort: currentState.sort, + startLevel: currentState.startLevel, + endLevel: currentState.endLevel ) - .andThen( - Observable.concat([ - .just(.initPage), - fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel), - .just(.setLastDeletedBookmark(nil)) - ]) + + case .fetchListFilter: + return fetchList( + sort: currentState.sort, + startLevel: currentState.startLevel ?? 1, + endLevel: currentState.endLevel ?? 200, + isFilter: true ) + + case .undoLastDeletedBookmark: + return handleUndoLastDeletedBookmark() + case .itemFilterOptionSelected(let results): - let criteria = parseItemFilterResultUseCase.execute(results: results) - return .concat([ - .just(.setJobId(criteria.jobIds)), - .just(.setFilter(start: criteria.startLevel, end: criteria.endLevel)), - .just(.setCategoryId(criteria.categoryIds)), - .just(.initPage) - ]) - .concat(Observable.deferred { [weak self] in - guard let self = self else { return .empty() } - return self.fetchListFilter( - sort: self.currentState.sort, - startLevel: self.currentState.startLevel, - endLevel: self.currentState.endLevel - ) - }) + return handleItemFilterOptionSelected(results: results) } } - // MARK: - Fetch List (필터 적용) - private func fetchListFilter(sort: String?, startLevel: Int?, endLevel: Int?) -> Observable { + // MARK: - Fetch (완전 통합) + private func fetchList( + sort: String?, + startLevel: Int?, + endLevel: Int?, + isFilter: Bool = false + ) -> Observable { + let response: Observable + switch currentState.type { case .monster: - return dictionaryListUseCase - .execute(type: .monster, query: DictionaryListQuery(keyword: currentState.keyword, page: currentState.currentPage, size: 20, sort: sort, minLevel: startLevel, maxLevel: endLevel)) - .map { Mutation.setFilterMonsterItem($0) } - case .item: - return dictionaryItemListUseCase - .execute( - keyword: currentState.keyword, - jobId: currentState.jobId, - minLevel: currentState.startLevel, - maxLevel: currentState.endLevel, - categoryIds: currentState.categoryIds, + response = dictionaryListUseCase.execute( + type: .monster, + query: DictionaryListQuery( + keyword: currentState.keyword ?? "", page: currentState.currentPage, size: 20, - sort: sort + sort: sort, + minLevel: startLevel, + maxLevel: endLevel ) - .map { Mutation.setFilterItemsItem($0) } - default: - return .empty() - } - } + ) - private func fetchList(sort: String?, startLevel: Int?, endLevel: Int?) -> Observable { - switch currentState.type { - case .monster: - return dictionaryListUseCase - .execute(type: .monster, query: DictionaryListQuery(keyword: currentState.keyword ?? "", page: currentState.currentPage, size: 20, sort: sort, minLevel: startLevel, maxLevel: endLevel)) - .map { Mutation.setListItem($0) } case .item: - return dictionaryItemListUseCase - .execute( - keyword: currentState.keyword, - jobId: currentState.jobId, - minLevel: currentState.minLevel, - maxLevel: currentState.maxLevel, - categoryIds: currentState.categoryIds, - page: currentState.currentPage, - size: 20, - sort: sort - ) - .map { Mutation.setListItem($0) } + response = dictionaryItemListUseCase.execute( + keyword: currentState.keyword ?? "", + jobId: currentState.jobId, + minLevel: currentState.minLevel, + maxLevel: currentState.maxLevel, + categoryIds: currentState.categoryIds, + page: currentState.currentPage, + size: 20, + sort: sort + ) + case .map: - return dictionaryMapListUseCase - .execute(keyword: currentState.keyword ?? "", page: currentState.currentPage, size: 20, sort: "ASC") - .map { Mutation.setListItem($0) } + response = dictionaryMapListUseCase.execute( + keyword: currentState.keyword ?? "", + page: currentState.currentPage, + size: 20, + sort: sort ?? "ASC" + ) + case .npc: - return dictionaryNpcListUseCase - .execute(keyword: currentState.keyword ?? "", page: currentState.currentPage, size: 20, sort: "ASC") - .map { Mutation.setListItem($0) } + response = dictionaryNpcListUseCase.execute( + keyword: currentState.keyword ?? "", + page: currentState.currentPage, + size: 20, + sort: sort ?? "ASC" + ) + case .quest: - return dictionaryQuestListUseCase - .execute(keyword: currentState.keyword ?? "", page: currentState.currentPage, size: 20, sort: "ASC") - .map { Mutation.setListItem($0) } + response = dictionaryQuestListUseCase.execute( + keyword: currentState.keyword ?? "", + page: currentState.currentPage, + size: 20, + sort: sort ?? "ASC" + ) + case .total: - return dictionaryAllListUseCase.execute(keyword: currentState.keyword ?? "", page: currentState.currentPage).map { Mutation.setListItem($0) } + response = dictionaryAllListUseCase.execute( + keyword: currentState.keyword ?? "", + page: currentState.currentPage + ) + default: return .empty() } + + return response.map { res in + if isFilter { + switch self.currentState.type { + case .monster: + return .setFilterMonsterItem(res) + case .item: + return .setFilterItemsItem(res) + default: + return .setListItem(res) + } + } else { + return .setListItem(res) + } + } } // MARK: - Reduce public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case let .setFilterMonsterItem(items): - newState.listItems = items.contents - case let .setFilterItemsItem(items): + case let .setFilterMonsterItem(items), + let .setFilterItemsItem(items): newState.listItems = items.contents case .showSortFilter: newState.route = .sort(newState.type) @@ -360,7 +268,7 @@ open class DictionaryListReactor: Reactor { newState.currentPage = 0 case let .setLastDeletedBookmark(item): newState.lastDeletedBookmark = item - case .setLoginState(let isLogin): + case let .setLoginState(isLogin): newState.isLogin = isLogin case .setJobId(let id): newState.jobId = id @@ -370,3 +278,98 @@ open class DictionaryListReactor: Reactor { return newState } } + +// MARK: - Methods +private extension DictionaryListReactor { + func handleViewWillAppear() -> Observable { + let loginState = checkLoginUseCase.execute() + .map { Mutation.setLoginState($0) } + + let fetchMutation = fetchList( + sort: currentState.sort, + startLevel: currentState.startLevel, + endLevel: currentState.endLevel + ) + + return .merge([loginState, fetchMutation]) + } + + func handleToggleBookmark(id: Int, isSelected: Bool) -> Observable { + guard let bookmarkItem = currentState.listItems.first(where: { $0.id == id }) else { return .empty() } + let bookmarkId = bookmarkItem.bookmarkId ?? 0 + + let saveDeletedMutation: Observable = isSelected + ? .just(.setLastDeletedBookmark(bookmarkItem)) + : .just(.setLastDeletedBookmark(nil)) + + return saveDeletedMutation.concat( + setBookmarkUseCase.execute( + bookmarkId: isSelected ? bookmarkId : id, + isBookmark: isSelected ? .delete : .set(bookmarkItem.type) + ) + .andThen( + Observable.concat([ + .just(.initPage), + fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) + ]) + ) + ) + } + + func handleSortOptionSelected(sort: SortType) -> Observable { + return .concat([ + .just(.setSort(sort.sortParameter)), + .just(.initPage) + ]) + } + + func handleFilterOptionSelected(startLevel: Int?, endLevel: Int?) -> Observable { + return .concat([ + .just(.setFilter(start: startLevel, end: endLevel)), + .just(.initPage) + ]) + .concat(Observable.deferred { [weak self] in + guard let self = self else { return .empty() } + return self.fetchList( + sort: self.currentState.sort, + startLevel: startLevel, + endLevel: endLevel, + isFilter: true + ) + }) + } + + func handleUndoLastDeletedBookmark() -> Observable { + guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } + return setBookmarkUseCase.execute( + bookmarkId: lastDeleted.id, + isBookmark: .set(lastDeleted.type) + ) + .andThen( + Observable.concat([ + .just(.initPage), + fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel), + .just(.setLastDeletedBookmark(nil)) + ]) + ) + } + + func handleItemFilterOptionSelected(results: [(String, String)]) -> Observable { + let criteria = parseItemFilterResultUseCase.execute(results: results) + return .concat([ + .just(.setJobId(criteria.jobIds)), + .just(.setFilter(start: criteria.startLevel, end: criteria.endLevel)), + .just(.setCategoryId(criteria.categoryIds)), + .just(.initPage) + ]) + .concat(Observable.deferred { [weak self] in + guard let self = self else { return .empty() } + return self.fetchList( + sort: self.currentState.sort, + startLevel: self.currentState.startLevel, + endLevel: self.currentState.endLevel, + isFilter: true + ) + }) + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift index 00e1d9ea..9c827b39 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift @@ -1,13 +1,12 @@ -import UIKit - +import AuthFeatureInterface import BaseFeature import BookmarkFeatureInterface import DictionaryFeatureInterface import DomainInterface - import ReactorKit import RxCocoa import RxSwift +import UIKit public final class DictionaryListViewController: BaseViewController, View { public typealias Reactor = DictionaryListReactor @@ -20,6 +19,7 @@ public final class DictionaryListViewController: BaseViewController, View { private let bookmarkModalFactory: BookmarkModalFactory private let sortedFactory: SortedBottomSheetFactory private let detailFactory: DictionaryDetailFactory + private let loginFactory: LoginFactory private var selectedSortIndex = 0 public let itemCountRelay = PublishRelay() @@ -27,13 +27,22 @@ public final class DictionaryListViewController: BaseViewController, View { // MARK: - Components private var mainView: DictionaryListView - public init(reactor: DictionaryListReactor, itemFilterFactory: ItemFilterBottomSheetFactory, monsterFilterFactory: MonsterFilterBottomSheetFactory, sortedFactory: SortedBottomSheetFactory, bookmarkModalFactory: BookmarkModalFactory, detailFactory: DictionaryDetailFactory) { + public init( + reactor: DictionaryListReactor, + itemFilterFactory: ItemFilterBottomSheetFactory, + monsterFilterFactory: MonsterFilterBottomSheetFactory, + sortedFactory: SortedBottomSheetFactory, + bookmarkModalFactory: BookmarkModalFactory, + detailFactory: DictionaryDetailFactory, loginFactory: LoginFactory + ) { self.itemFilterFactory = itemFilterFactory self.monsterFilterFactory = monsterFilterFactory self.sortedFactory = sortedFactory self.bookmarkModalFactory = bookmarkModalFactory self.detailFactory = detailFactory - self.mainView = DictionaryListView(isFilterHidden: reactor.currentState.type.isSortHidden) + self.loginFactory = loginFactory + self.mainView = DictionaryListView( + isFilterHidden: reactor.currentState.type.isSortHidden) super.init() self.reactor = reactor } @@ -54,31 +63,35 @@ public final class DictionaryListViewController: BaseViewController, View { } // MARK: - SetUp -private extension DictionaryListViewController { - func addViews() { +extension DictionaryListViewController { + fileprivate func addViews() { view.addSubview(mainView) } - func setupConstraints() { + fileprivate func setupConstraints() { mainView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide) make.horizontalEdges.bottom.equalToSuperview() } } - func configureUI() { + fileprivate func configureUI() { mainView.listCollectionView.collectionViewLayout = createListLayout() mainView.listCollectionView.delegate = self mainView.listCollectionView.dataSource = self - mainView.listCollectionView.register(DictionaryListCell.self, forCellWithReuseIdentifier: DictionaryListCell.identifier) + mainView.listCollectionView.register( + DictionaryListCell.self, + forCellWithReuseIdentifier: DictionaryListCell.identifier) } - func createListLayout() -> UICollectionViewLayout { + fileprivate func createListLayout() -> UICollectionViewLayout { let layoutFactory = LayoutFactory() let layout = CompositionalLayoutBuilder() .section { _ in layoutFactory.getDictionaryListLayout() } .build() - layout.register(Neutral300DividerView.self, forDecorationViewOfKind: Neutral300DividerView.identifier) + layout.register( + Neutral300DividerView.self, + forDecorationViewOfKind: Neutral300DividerView.identifier) return layout } } @@ -123,8 +136,8 @@ extension DictionaryListViewController { }) .disposed(by: disposeBag) - rx.viewDidLoad - .map { _ in Reactor.Action.viewDidLoad } + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -135,7 +148,10 @@ extension DictionaryListViewController { .subscribe { owner, route in switch route { case .sort(let type): - let viewController = owner.sortedFactory.make(sortedOptions: type.bookmarkSortedFilter, selectedIndex: owner.selectedSortIndex) { index in + let viewController = owner.sortedFactory.make( + sortedOptions: type.bookmarkSortedFilter, + selectedIndex: owner.selectedSortIndex + ) { index in owner.selectedSortIndex = index let selectedFilter = reactor.currentState.type.bookmarkSortedFilter[index] reactor.action.onNext(.sortOptionSelected(selectedFilter)) @@ -183,15 +199,22 @@ extension DictionaryListViewController { } // MARK: - Delegate -extension DictionaryListViewController: UICollectionViewDelegate, UICollectionViewDataSource { - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { +extension DictionaryListViewController: UICollectionViewDelegate, + UICollectionViewDataSource { + public func collectionView( + _ collectionView: UICollectionView, numberOfItemsInSection section: Int + ) -> Int { guard let state = reactor?.currentState else { return 0 } return state.listItems.count } - public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let state = reactor?.currentState else { return UICollectionViewCell() } + public func collectionView( + _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + guard let state = reactor?.currentState else { + return UICollectionViewCell() + } guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: DictionaryListCell.identifier, @@ -203,7 +226,8 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi let item = state.listItems[indexPath.row] var subText: String? { - [.item, .monster, .quest].contains(item.type) ? item.level.map { "Lv. \($0)" } : nil + [.item, .monster, .quest].contains(item.type) + ? item.level.map { "Lv. \($0)" } : nil } cell.inject( type: .bookmark, @@ -223,17 +247,19 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi ctaText: "로그인 하기", cancelText: "취소", ctaAction: { - print("로그인 화면으로 이동") + let viewController = self.loginFactory.make( + exitRoute: .pop) + self.navigationController?.pushViewController( + viewController, animated: true) }, - cancelAction: { - print("취소됨") - } + cancelAction: nil ) return } if item.bookmarkId != nil { - self.reactor?.action.onNext(.toggleBookmark(item.id, isSelected)) + self.reactor?.action.onNext( + .toggleBookmark(item.id, isSelected)) SnackBarFactory.createSnackBar( type: .delete, imageUrl: item.imageUrl, @@ -241,11 +267,13 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi text: "아이템을 북마크에서 삭제했어요.", buttonText: "되돌리기", buttonAction: { [weak self] in - self?.reactor?.action.onNext(.undoLastDeletedBookmark) + self?.reactor?.action.onNext( + .undoLastDeletedBookmark) } ) } else { - self.reactor?.action.onNext(.toggleBookmark(item.id, isSelected)) + self.reactor?.action.onNext( + .toggleBookmark(item.id, isSelected)) SnackBarFactory.createSnackBar( type: .normal, imageUrl: item.imageUrl, @@ -254,16 +282,20 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi buttonText: "컬렉션 추가", buttonAction: { DispatchQueue.main.async { - let viewController = self.bookmarkModalFactory.make( - onDismissWithColletions: { _ in }, - onDismissWithMessage: { _ in - ToastFactory.createToast( - message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." - ) - } - ) - viewController.modalPresentationStyle = .pageSheet - if let sheet = viewController.sheetPresentationController { + let viewController = self.bookmarkModalFactory + .make( + onDismissWithColletions: { _ in }, + onDismissWithMessage: { _ in + ToastFactory.createToast( + message: + "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." + ) + } + ) + viewController.modalPresentationStyle = + .pageSheet + if let sheet = viewController + .sheetPresentationController { sheet.detents = [.medium(), .large()] sheet.prefersGrabberVisible = true sheet.preferredCornerRadius = 16 @@ -279,7 +311,9 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi return cell } - public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + public func collectionView( + _ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath + ) { guard let reactor = reactor else { return } let item: DictionaryMainItemResponse @@ -288,24 +322,12 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi switch reactor.currentState.type { case .total: - // 전체 타입일 때는 item.type에 따라 분기 - switch item.type { - case .monster: - viewController = detailFactory.make(type: .monster, id: item.id) - case .item: - viewController = detailFactory.make(type: .item, id: item.id) - case .npc: - viewController = detailFactory.make(type: .npc, id: item.id) - case .quest: - viewController = detailFactory.make(type: .quest, id: item.id) - case .map: - viewController = detailFactory.make(type: .map, id: item.id) - default: - return // 알 수 없는 타입이면 무시 - } + guard let type = item.type.toDictionaryType else { return } + viewController = detailFactory.make(type: type, id: item.id) default: // 단일 타입일 경우 리액터 타입에 따라 처리 - viewController = detailFactory.make(type: reactor.currentState.type, id: item.id) + viewController = detailFactory.make( + type: reactor.currentState.type, id: item.id) } navigationController?.pushViewController(viewController, animated: true) } @@ -316,8 +338,8 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi let height = scrollView.frame.size.height if offsetY > contentHeight - height - 100 { - reactor?.action.onNext(.setCurrentPage) // 페이지 올리고 - reactor?.action.onNext(.fetchList) // 해당 페이지로 데이터 불러오기 + reactor?.action.onNext(.setCurrentPage) // 페이지 올리고 + reactor?.action.onNext(.fetchList) // 해당 페이지로 데이터 불러오기 } } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift index ca0d1269..54286068 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift @@ -8,6 +8,7 @@ public final class DictionaryMainReactor: Reactor { case none case search case notification + case login } public enum Action { @@ -31,9 +32,12 @@ public final class DictionaryMainReactor: Reactor { public var initialState: State var disposeBag = DisposeBag() + private let checkLoginUseCase: CheckLoginUseCase + // MARK: - init - public init() { + public init(checkLoginUseCase: CheckLoginUseCase) { self.initialState = State() + self.checkLoginUseCase = checkLoginUseCase } // MARK: - Reactor Methods @@ -42,7 +46,14 @@ public final class DictionaryMainReactor: Reactor { case .searchButtonTapped: return Observable.just(.navigateTo(.search)) case .notificationButtonTapped: - return Observable.just(.navigateTo(.notification)) + return checkLoginUseCase.execute() + .map { isLogin in + if isLogin { + return .navigateTo(.notification) + } else { + return .navigateTo(.login) + } + } } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift index df13a133..dd969c9c 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift @@ -14,6 +14,7 @@ public final class DictionaryMainViewController: BaseViewController, View { // MARK: - Properties public var disposeBag = DisposeBag() + private let initialIndex: Int private lazy var currentPageIndex = BehaviorRelay(value: initialIndex) @@ -135,6 +136,7 @@ public extension DictionaryMainViewController { .take(1) .flatMapLatest { _ in return reactor.pulse(\.$route) } .withUnretained(self) + .observe(on: MainScheduler.instance) .subscribe { (owner, route) in switch route { case .search: diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift index a941c64b..579cc17b 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift @@ -6,15 +6,17 @@ public final class DictionaryMainViewFactoryImpl: DictionaryMainViewFactory { private let dictionaryMainListFactory: DictionaryMainListFactory private let searchFactory: DictionarySearchFactory private let notificationFactory: DictionaryNotificationFactory + private let checkLoginUseCase: CheckLoginUseCase - public init(dictionaryMainListFactory: DictionaryMainListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory) { + public init(dictionaryMainListFactory: DictionaryMainListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory, checkLoginUseCase: CheckLoginUseCase) { self.dictionaryMainListFactory = dictionaryMainListFactory self.searchFactory = searchFactory self.notificationFactory = notificationFactory + self.checkLoginUseCase = checkLoginUseCase } public func make() -> BaseViewController { - let reactor = DictionaryMainReactor() + let reactor = DictionaryMainReactor(checkLoginUseCase: checkLoginUseCase) let viewController = DictionaryMainViewController(dictionaryMainListFactory: dictionaryMainListFactory, searchFactory: searchFactory, notificationFactory: notificationFactory, reactor: reactor) viewController.reactor = reactor return viewController diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift index eb4b53fb..d7875a36 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift @@ -4,16 +4,18 @@ import DomainInterface import MyPageFeatureInterface public final class DictionaryNotificationFactoryImpl: DictionaryNotificationFactory { - private let fetchNotificationUseCase: FetchNotificationUseCase private let notificationSettingFactory: NotificationSettingFactory + private let fetchAllAlarmUseCase: FetchAllAlarmUseCase + private let fetchProfileUseCase: FetchProfileUseCase - public init(fetchNotificationUseCase: FetchNotificationUseCase, notificationSettingFactory: NotificationSettingFactory) { - self.fetchNotificationUseCase = fetchNotificationUseCase + public init(notificationSettingFactory: NotificationSettingFactory, fetchAllAlarmUseCase: FetchAllAlarmUseCase, fetchProfileUseCase: FetchProfileUseCase) { self.notificationSettingFactory = notificationSettingFactory + self.fetchAllAlarmUseCase = fetchAllAlarmUseCase + self.fetchProfileUseCase = fetchProfileUseCase } public func make() -> BaseViewController { - let reactor = DictionaryNotificationReactor(fetchNotificationUseCase: fetchNotificationUseCase) + let reactor = DictionaryNotificationReactor(fetchAllAlarmUseCase: fetchAllAlarmUseCase, fetchProfileUseCase: fetchProfileUseCase) let viewController = DictionaryNotificationViewController(notificationSettingFactory: notificationSettingFactory) viewController.reactor = reactor return viewController diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift index a0693030..36a40a99 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift @@ -1,6 +1,6 @@ -import ReactorKit - import DomainInterface +import ReactorKit +import RxSwift public final class DictionaryNotificationReactor: Reactor { // MARK: - Reactor @@ -12,58 +12,96 @@ public final class DictionaryNotificationReactor: Reactor { } public enum Action { - case backbuttonTapped + case viewWillAppear + case loadMore + case backButtonTapped case settingButtonTapped case notificationTapped(String) - case viewWillAppear } public enum Mutation { + case setNotifications([AllAlarmResponse], hasMore: Bool, reset: Bool) + case setLoading(Bool) + case setProfile(MyPageResponse?) case navigateTo(Route) - case setNotifications([Notification]) } public struct State { @Pulse var route: Route = .none - var notifications: [Notification] = [] - var isAgreeNotification: Bool = true + var notifications: [AllAlarmResponse] = [] + var profile: MyPageResponse? + var hasMore: Bool = false + var isLoading: Bool = false } - // MARK: - properties + // MARK: - Properties public var initialState: State - var disposeBag = DisposeBag() - - private let fetchNotificationUseCase: FetchNotificationUseCase + private let disposeBag = DisposeBag() + private let fetchAllAlarmUseCase: FetchAllAlarmUseCase + private let fetchProfileUseCase: FetchProfileUseCase - // MARK: - init - public init(fetchNotificationUseCase: FetchNotificationUseCase) { + // MARK: - Init + public init(fetchAllAlarmUseCase: FetchAllAlarmUseCase, fetchProfileUseCase: FetchProfileUseCase) { self.initialState = State() - self.fetchNotificationUseCase = fetchNotificationUseCase + self.fetchAllAlarmUseCase = fetchAllAlarmUseCase + self.fetchProfileUseCase = fetchProfileUseCase } - // MARK: - Reactor Methods + // MARK: - Mutate public func mutate(action: Action) -> Observable { switch action { - case .backbuttonTapped: - return Observable.just(.navigateTo(.dismiss)) + case .viewWillAppear: + return .concat([ + .just(.setLoading(true)), + fetchAllAlarmUseCase.execute(cursor: nil, pageSize: 20) + .map { paged in + .setNotifications(paged.items, hasMore: paged.hasMore, reset: true) + }, + .just(.setLoading(false)) + ]) + case .loadMore: + guard currentState.hasMore, !currentState.isLoading else { return .empty() } + let cursor = currentState.notifications.last?.date + + return .concat([ + .just(.setLoading(true)), + fetchAllAlarmUseCase.execute(cursor: cursor, pageSize: 20) + .map { paged in + .setNotifications(paged.items, hasMore: paged.hasMore, reset: false) + }, + .just(.setLoading(false)) + ]) + + case .backButtonTapped: + return .just(.navigateTo(.dismiss)) case .settingButtonTapped: - return Observable.just(.navigateTo(.setting)) + return .just(.navigateTo(.setting)) case .notificationTapped(let notification): - return Observable.just(.navigateTo(.notification(notification))) - case .viewWillAppear: - return fetchNotificationUseCase.execute() - .map { Mutation.setNotifications($0) } + return .just(.navigateTo(.notification(notification))) } } + // MARK: - Reduce public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case .navigateTo(let route): + case let .setNotifications(newItems, hasMore, reset): + if reset { + newState.notifications = newItems + } else { + newState.notifications.append(contentsOf: newItems) + } + newState.hasMore = hasMore + + case let .setLoading(isLoading): + newState.isLoading = isLoading + + case let .setProfile(profile): + newState.profile = profile + + case let .navigateTo(route): newState.route = route - case .setNotifications(let notifications): - newState.notifications = notifications } return newState diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift index a8ffe35b..da8b6589 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift @@ -57,7 +57,7 @@ private extension DictionaryNotificationViewController { func configureUI() { isBottomTabbarHidden = true guard let reactor = reactor else { return } - mainView.setEmpty(isEmpty: reactor.currentState.isAgreeNotification) + mainView.setEmpty(isEmpty: reactor.currentState.profile?.noticeAgreement == false) mainView.notificationCollectionView.delegate = self mainView.notificationCollectionView.dataSource = self @@ -81,8 +81,13 @@ public extension DictionaryNotificationViewController { } func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + mainView.header.leftButton.rx.tap - .map { Reactor.Action.backbuttonTapped } + .map { Reactor.Action.backButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -102,7 +107,9 @@ public extension DictionaryNotificationViewController { case .dismiss: owner.navigationController?.popViewController(animated: true) case .setting: - let viewController = owner.notificationSettingFactory.make() + guard let reactor = owner.reactor, + let profile = reactor.currentState.profile else { return } + let viewController = owner.notificationSettingFactory.make(isAgreeEventNotification: profile.eventAgreement, isAgreeNoticeNotification: profile.noticeAgreement, isAgreePatchNoteNotification: profile.patchNoteAgreement) owner.navigationController?.pushViewController(viewController, animated: true) default: break @@ -110,12 +117,14 @@ public extension DictionaryNotificationViewController { } .disposed(by: disposeBag) - rx.viewWillAppear - .take(1) - .map { _ in Reactor.Action.viewWillAppear } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } + reactor.state.map { $0.notifications } + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, _ in + owner.mainView.notificationCollectionView.reloadData() + } + .disposed(by: disposeBag) } } // MARK: - Delegate @@ -129,7 +138,7 @@ extension DictionaryNotificationViewController: UICollectionViewDelegate, UIColl guard let reactor = reactor else { return UICollectionViewCell() } guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DictionaryNotificationCell.identifier, for: indexPath) as? DictionaryNotificationCell else { return UICollectionViewCell() } let item = reactor.currentState.notifications[indexPath.row] - cell.inject(input: DictionaryNotificationCell.Input(title: item.title, subTitle: item.date, isChecked: item.isChecked)) + cell.inject(input: DictionaryNotificationCell.Input(title: item.title, subTitle: item.date.changeKoreanDate(), isChecked: item.alreadyRead)) return cell } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift index 68b39c92..5f7cb884 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift @@ -16,7 +16,7 @@ public final class DictionarySearchReactor: Reactor { } public enum Action { - case viewDidLoad + case viewWillAppear case backButtonTapped case searchButtonTapped(String) case cancelRecentButtonTapped(String) @@ -91,7 +91,7 @@ public final class DictionarySearchReactor: Reactor { // MARK: - Reactor Methods public func mutate(action: Action) -> Observable { switch action { - case .viewDidLoad: + case .viewWillAppear: return recentSearchFetchUseCase.fetch().map { Mutation.setRecentList($0) } case .backButtonTapped: return Observable.just(.navigateTo(.dismiss)) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift index 357dcc78..9693f932 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift @@ -13,11 +13,12 @@ public final class DictionarySearchViewController: BaseViewController, View { public typealias Reactor = DictionarySearchReactor // MARK: - Properties + public var disposeBag = DisposeBag() + private var searchResultFactory: DictionarySearchResultFactory private let chipTapRelay = PublishRelay() private let chipCancelRelay = PublishRelay() - public var disposeBag = DisposeBag() // MARK: - Components private let mainView = DictionarySearchView() @@ -122,6 +123,12 @@ extension DictionarySearchViewController { } func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .take(1) + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + mainView.searchBar.backButton.rx.tap .map { Reactor.Action.backButtonTapped } .bind(to: reactor.action) @@ -176,13 +183,6 @@ extension DictionarySearchViewController { } } .disposed(by: disposeBag) - - rx.viewDidLoad - .take(1) - .map { Reactor.Action.viewDidLoad } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearchResult/DictionarySearchResultViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearchResult/DictionarySearchResultViewController.swift index 7b1cfa36..fa38a881 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearchResult/DictionarySearchResultViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearchResult/DictionarySearchResultViewController.swift @@ -14,6 +14,7 @@ public final class DictionarySearchResultViewController: BaseViewController, Vie // MARK: - Properties public var disposeBag = DisposeBag() + private let initialIndex: Int private lazy var currentPageIndex = BehaviorRelay(value: initialIndex) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetViewController.swift index ebbd4e11..920a886b 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetViewController.swift @@ -1,3 +1,5 @@ +// swiftlint:disable all + import UIKit import BaseFeature @@ -456,7 +458,6 @@ extension ItemFilterBottomSheetViewController { } .bind(to: reactor.action) .disposed(by: disposeBag) - } func bindViewState(reactor: Reactor) { diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/SortedBottomSheet/SortedBottomSheetViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/SortedBottomSheet/SortedBottomSheetViewController.swift index 8d92b536..a5e5ae06 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/SortedBottomSheet/SortedBottomSheetViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/SortedBottomSheet/SortedBottomSheetViewController.swift @@ -16,6 +16,7 @@ public final class SortedBottomSheetViewController: BaseViewController, ModalPre // MARK: - Properties public var disposeBag = DisposeBag() + public var onSelectedIndex: ((Int) -> Void)? // MARK: - Components diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeatureDemo/AppDelegate.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeatureDemo/AppDelegate.swift index 48195a79..1f04fe03 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeatureDemo/AppDelegate.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeatureDemo/AppDelegate.swift @@ -1,4 +1,5 @@ // swiftlint:disable function_body_length +// swiftlint:disable line_length import UIKit @@ -72,10 +73,12 @@ private extension AppDelegate { DIContainer.register(type: BookmarkRepository.self) { BookmarkRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), interceptor: TokenInterceptor(fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self))) } - DIContainer.register(type: UserDefaultsRepository.self) { UserDefaultsRepositoryImpl() } + DIContainer.register(type: AlarmAPIRepository.self) { + AlarmAPIRepositoryImpl(provider: DIContainer.resolve(type: NetworkProvider.self), interceptor: TokenInterceptor(fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self))) + } } func registerUseCase() { @@ -97,10 +100,10 @@ private extension AppDelegate { FetchJobListUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) } DIContainer.register(type: LoginWithAppleUseCase.self) { - LoginWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + LoginWithAppleUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self), userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self)) } DIContainer.register(type: LoginWithKakaoUseCase.self) { - LoginWithKakaoUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + LoginWithKakaoUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self), userDefaultsRepository: DIContainer.resolve(type: UserDefaultsRepository.self)) } DIContainer.register(type: SignUpWithAppleUseCase.self) { SignUpWithAppleUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) @@ -177,9 +180,6 @@ private extension AppDelegate { DIContainer.register(type: FetchDictionaryDetailMapNpcUseCase.self) { FetchDictionaryDetailMapNpcUseCaseImpl(repository: DIContainer.resolve(type: DictionaryDetailAPIRepository.self)) } - DIContainer.register(type: FetchNotificationUseCase.self) { - FetchNotificationUseCaseImpl() - } DIContainer.register(type: CheckLoginUseCase.self) { CheckLoginUseCaseImpl(authRepository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self)) } @@ -234,6 +234,18 @@ private extension AppDelegate { DIContainer.register(type: ParseItemFilterResultUseCase.self) { ParseItemFilterResultUseCaseImpl() } + DIContainer.register(type: FetchPlatformUseCase.self) { + FetchPlatformUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } + DIContainer.register(type: FetchProfileUseCase.self) { + FetchProfileUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self), fetchJobUseCase: DIContainer.resolve(type: FetchJobUseCase.self)) + } + DIContainer.register(type: FetchJobUseCase.self) { + FetchJobUseCaseImpl(repository: DIContainer.resolve(type: AuthAPIRepository.self)) + } + DIContainer.register(type: FetchAllAlarmUseCase.self) { + FetchAllAlarmUseCaseImpl(repository: DIContainer.resolve(type: AlarmAPIRepository.self)) + } } func registerFactory() { DIContainer.register(type: ItemFilterBottomSheetFactory.self) { @@ -249,7 +261,7 @@ private extension AppDelegate { BookmarkModalFactoryImpl(addCollectionFactory: DIContainer.resolve(type: AddCollectionFactory.self)) } DIContainer.register(type: DictionaryDetailFactory.self) { - DictionaryDetailFactoryImpl(dictionaryDetailMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapUseCase.self), dictionaryDetailMapSpawnMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapSpawnMonsterUseCase.self), dictionaryDetailMapNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapNpcUseCase.self), dictionaryDetailQuestLinkedQuestsUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestLinkedQuestsUseCase.self), dictionaryDetailQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestUseCase.self), dictionaryDetailItemDropMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemDropMonsterUseCase.self), dictionaryDetailItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemUseCase.self), dictionaryDetailNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcUseCase.self), dictionaryDetailNpcQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcQuestUseCase.self), dictionaryDetailNpcMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcMapUseCase.self), dictionaryDetailMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterUseCase.self), dictionaryDetailMonsterDropItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterItemsUseCase.self), dictionaryDetailMonsterMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterMapUseCase.self)) + DictionaryDetailFactoryImpl(loginFactory: { DIContainer.resolve(type: LoginFactory.self) }, bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), dictionaryDetailMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapUseCase.self), dictionaryDetailMapSpawnMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapSpawnMonsterUseCase.self), dictionaryDetailMapNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailMapNpcUseCase.self), dictionaryDetailQuestLinkedQuestsUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestLinkedQuestsUseCase.self), dictionaryDetailQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailQuestUseCase.self), dictionaryDetailItemDropMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemDropMonsterUseCase.self), dictionaryDetailItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailItemUseCase.self), dictionaryDetailNpcUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcUseCase.self), dictionaryDetailNpcQuestUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcQuestUseCase.self), dictionaryDetailNpcMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailNpcMapUseCase.self), dictionaryDetailMonsterUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterUseCase.self), dictionaryDetailMonsterDropItemUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterItemsUseCase.self), dictionaryDetailMonsterMapUseCase: DIContainer.resolve(type: FetchDictionaryDetailMonsterMapUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self)) } DIContainer.register(type: DictionaryMainListFactory.self) { DictionaryListFactoryImpl( @@ -266,7 +278,7 @@ private extension AppDelegate { monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), - detailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self) + detailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self), loginFactory: { DIContainer.resolve(type: LoginFactory.self) } ) } DIContainer.register(type: DictionarySearchResultFactory.self) { @@ -288,12 +300,7 @@ private extension AppDelegate { NotificationSettingFactoryImpl(checkNotificationPermissionUseCase: DIContainer.resolve(type: CheckNotificationPermissionUseCase.self), updateNotificationAgreementUseCase: DIContainer.resolve(type: UpdateNotificationAgreementUseCase.self)) } DIContainer.register(type: DictionaryNotificationFactory.self) { - DictionaryNotificationFactoryImpl( - fetchNotificationUseCase: DIContainer - .resolve(type: FetchNotificationUseCase.self), - notificationSettingFactory: DIContainer - .resolve(type: NotificationSettingFactory.self) - ) + DictionaryNotificationFactoryImpl(notificationSettingFactory: DIContainer.resolve(type: NotificationSettingFactory.self), fetchAllAlarmUseCase: DIContainer.resolve(type: FetchAllAlarmUseCase.self), fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self)) } DIContainer.register(type: DictionaryMainViewFactory.self) { DictionaryMainViewFactoryImpl( @@ -301,8 +308,7 @@ private extension AppDelegate { .resolve(type: DictionaryMainListFactory.self), searchFactory: DIContainer.resolve(type: DictionarySearchFactory.self), notificationFactory: DIContainer - .resolve(type: DictionaryNotificationFactory.self) - ) + .resolve(type: DictionaryNotificationFactory.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self)) } DIContainer.register(type: BookmarkOnBoardingFactory.self) { BookmarkOnBoardingFactoryImpl() @@ -358,14 +364,14 @@ private extension AppDelegate { loginWithKakaoUseCase: DIContainer .resolve(type: LoginWithKakaoUseCase.self), fetchTokenUseCase: DIContainer.resolve(type: FetchTokenFromLocalUseCase.self), - putFCMTokenUseCase: DIContainer.resolve(type: PutFCMTokenUseCase.self) + putFCMTokenUseCase: DIContainer.resolve(type: PutFCMTokenUseCase.self), fetchPlatformUseCase: DIContainer.resolve(type: FetchPlatformUseCase.self) ) } DIContainer.register(type: AddCollectionFactory.self) { AddCollectionFactoryImpl() } DIContainer.register(type: BookmarkListFactory.self) { - BookmarkListFactoryImpl(itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), loginFactory: DIContainer.resolve(type: LoginFactory.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), fetchBookmarkUseCase: DIContainer.resolve(type: FetchBookmarkUseCase.self), fetchMonsterBookmarkUseCase: DIContainer.resolve(type: FetchMonsterBookmarkUseCase.self), fetchItemBookmarkUseCase: DIContainer.resolve(type: FetchItemBookmarkUseCase.self), fetchNPCBookmarkUseCase: DIContainer.resolve(type: FetchNPCBookmarkUseCase.self), fetchQuestBookmarkUseCase: DIContainer.resolve(type: FetchQuestBookmarkUseCase.self), fetchMapBookmarkUseCase: DIContainer.resolve(type: FetchMapBookmarkUseCase.self)) + BookmarkListFactoryImpl(itemFilterFactory: DIContainer.resolve(type: ItemFilterBottomSheetFactory.self), monsterFilterFactory: DIContainer.resolve(type: MonsterFilterBottomSheetFactory.self), sortedFactory: DIContainer.resolve(type: SortedBottomSheetFactory.self), bookmarkModalFactory: DIContainer.resolve(type: BookmarkModalFactory.self), loginFactory: DIContainer.resolve(type: LoginFactory.self), dictionaryDetailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self), setBookmarkUseCase: DIContainer.resolve(type: SetBookmarkUseCase.self), checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), fetchBookmarkUseCase: DIContainer.resolve(type: FetchBookmarkUseCase.self), fetchMonsterBookmarkUseCase: DIContainer.resolve(type: FetchMonsterBookmarkUseCase.self), fetchItemBookmarkUseCase: DIContainer.resolve(type: FetchItemBookmarkUseCase.self), fetchNPCBookmarkUseCase: DIContainer.resolve(type: FetchNPCBookmarkUseCase.self), fetchQuestBookmarkUseCase: DIContainer.resolve(type: FetchQuestBookmarkUseCase.self), fetchMapBookmarkUseCase: DIContainer.resolve(type: FetchMapBookmarkUseCase.self), collectionEditFactory: DIContainer.resolve(type: CollectionEditFactory.self)) } DIContainer.register(type: CollectionSettingFactory.self) { CollectionSettingFactoryImpl() @@ -383,7 +389,7 @@ private extension AppDelegate { addCollectionFactory: DIContainer .resolve(type: AddCollectionFactory.self), collectionEditFactory: DIContainer - .resolve(type: CollectionEditFactory.self) + .resolve(type: CollectionEditFactory.self), dictionaryDetailFactory: DIContainer.resolve(type: DictionaryDetailFactory.self) ) } DIContainer.register(type: CollectionListFactory.self) { diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature.xcodeproj/project.pbxproj b/MLS/Presentation/MyPageFeature/MyPageFeature.xcodeproj/project.pbxproj index db77c710..77c3b916 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature.xcodeproj/project.pbxproj +++ b/MLS/Presentation/MyPageFeature/MyPageFeature.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 7746ABB12E84245D0046F603 /* RxGesture in Frameworks */ = {isa = PBXBuildFile; productRef = 7746ABB02E84245D0046F603 /* RxGesture */; }; 7746ABB32E8425E00046F603 /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7746ABB22E8425E00046F603 /* DesignSystem.framework */; }; 7746ABB72E8427F80046F603 /* RxKeyboard in Frameworks */ = {isa = PBXBuildFile; productRef = 7746ABB62E8427F80046F603 /* RxKeyboard */; }; + 775966F32EC30B3A00CC389B /* AuthFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 775966F22EC30B3A00CC389B /* AuthFeatureInterface.framework */; }; 776FEA342EA4D29B0039ACE2 /* AuthFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 776FEA332EA4D29B0039ACE2 /* AuthFeatureInterface.framework */; }; 776FEA352EA4D29B0039ACE2 /* AuthFeatureInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 776FEA332EA4D29B0039ACE2 /* AuthFeatureInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 777C28142E7D87B8000765F2 /* RxKeyboard in Frameworks */ = {isa = PBXBuildFile; productRef = 777C28132E7D87B8000765F2 /* RxKeyboard */; }; @@ -108,6 +109,7 @@ 7746ABA92E84225A0046F603 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7746ABAC2E84227A0046F603 /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7746ABB22E8425E00046F603 /* DesignSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DesignSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 775966F22EC30B3A00CC389B /* AuthFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 776FEA332EA4D29B0039ACE2 /* AuthFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 77BE55B82E78596900522216 /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 77BE55B92E78596900522216 /* DomainInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DomainInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -159,6 +161,7 @@ 7746ABB32E8425E00046F603 /* DesignSystem.framework in Frameworks */, 773A3C092E71712300F75B30 /* MyPageFeatureInterface.framework in Frameworks */, 777C28142E7D87B8000765F2 /* RxKeyboard in Frameworks */, + 775966F32EC30B3A00CC389B /* AuthFeatureInterface.framework in Frameworks */, 773A3BEB2E716FD800F75B30 /* SnapKit in Frameworks */, 773A3F712E71736A00F75B30 /* BaseFeature.framework in Frameworks */, ); @@ -223,6 +226,7 @@ 773A3BF62E71701D00F75B30 /* Frameworks */ = { isa = PBXGroup; children = ( + 775966F22EC30B3A00CC389B /* AuthFeatureInterface.framework */, 776FEA332EA4D29B0039ACE2 /* AuthFeatureInterface.framework */, 7746ABB22E8425E00046F603 /* DesignSystem.framework */, 7746ABAC2E84227A0046F603 /* DomainInterface.framework */, diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift index 888f7c6c..8c89045d 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift @@ -12,18 +12,17 @@ import RxSwift */ class CustomerSupportBaseViewController: BaseViewController { // MARK: - Properties - public var disposeBag = DisposeBag() - // MARK: - Components - public var mainView = CustomerSupportBaseView() - public var type: CustomerSupportType - /// 현재 보여지고 있는 뷰의 인덱스 public var currentTabIndex: Int? public var urlStrings: [String] = [] var onItemTapped: ((Int) -> Void)? + // MARK: - Components + public var mainView = CustomerSupportBaseView() + public var type: CustomerSupportType + public init(type: CustomerSupportType) { self.type = type mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) @@ -45,7 +44,7 @@ class CustomerSupportBaseViewController: BaseViewController { func createDetailItem(items: [AlarmResponse]) { for (index, item) in items.enumerated() { - let view = mainView.createDetailItem(titleText: item.title, dateText: changeKoreanDate(date: item.date)) + let view = mainView.createDetailItem(titleText: item.title, dateText: item.date.changeKoreanDate()) view.tag = index urlStrings.append(item.link) @@ -76,10 +75,6 @@ class CustomerSupportBaseViewController: BaseViewController { .disposed(by: disposeBag) } } - - func changeKoreanDate(date: [Int]) -> String? { - return "\(date[0])년 \(date[1])월 \(date[2])일 \(date[3]):\(String(format: "%02d", date[4]))" - } } // MARK: - SetUp diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift index 29154718..89617b49 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift @@ -69,7 +69,7 @@ public final class MyPageMainReactor: Reactor { // MARK: - Mutation public enum Mutation { case toNavigate(Route) - case setProfile(MyPageResponse) + case setProfile(MyPageResponse?) } // MARK: - State @@ -113,6 +113,10 @@ public final class MyPageMainReactor: Reactor { case .viewWillAppear: return fetchProfileUseCase.execute() .map { .setProfile($0) } + .catch { error in + print(error) + return .just(.setProfile(nil)) + } } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift index 314ee9da..b8a09526 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift @@ -152,10 +152,12 @@ extension MyPageMainViewController { let viewController = owner.customerSupportFactory.make(type: .terms) owner.navigationController?.pushViewController(viewController, animated: true) case .notificationSetting: - let viewController = owner.notificationSettingFactory.make() + guard let reactor = owner.reactor, + let profile = reactor.currentState.profile else { return } + let viewController = owner.notificationSettingFactory.make(isAgreeEventNotification: profile.eventAgreement, isAgreeNoticeNotification: profile.noticeAgreement, isAgreePatchNoteNotification: profile.patchNoteAgreement) owner.navigationController?.pushViewController(viewController, animated: true) case .login: - let viewController = owner.loginFactory.make(isReLogin: false) + let viewController = owner.loginFactory.make(exitRoute: .pop) owner.navigationController?.pushViewController(viewController, animated: true) case .none: break diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingFactoryImpl.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingFactoryImpl.swift index c5646198..10548ef8 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingFactoryImpl.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingFactoryImpl.swift @@ -11,8 +11,16 @@ public final class NotificationSettingFactoryImpl: NotificationSettingFactory { self.updateNotificationAgreementUseCase = updateNotificationAgreementUseCase } - public func make() -> BaseViewController { - let viewController = NotificationSettingViewController(reactor: NotificationSettingReactor(checkNotificationPermissionUseCase: checkNotificationPermissionUseCase, updateNotificationAgreementUseCase: updateNotificationAgreementUseCase)) + public func make(isAgreeEventNotification: Bool, isAgreeNoticeNotification: Bool, isAgreePatchNoteNotification: Bool) -> BaseViewController { + let viewController = NotificationSettingViewController( + reactor: NotificationSettingReactor( + checkNotificationPermissionUseCase: checkNotificationPermissionUseCase, + updateNotificationAgreementUseCase: updateNotificationAgreementUseCase, + isAgreeEventNotification: isAgreeEventNotification, + isAgreeNoticeNotification: isAgreeNoticeNotification, + isAgreePatchNoteNotification: isAgreePatchNoteNotification + ) + ) return viewController } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift index d652c5f4..6596009d 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift @@ -30,9 +30,9 @@ public final class NotificationSettingReactor: Reactor { public struct State { @Pulse var route = Route.none var authorized = false - var isAgreeEventNotification = false - var isAgreeNoticeNotification = false - var isAgreePatchNoteNotification = false + var isAgreeEventNotification: Bool + var isAgreeNoticeNotification: Bool + var isAgreePatchNoteNotification: Bool } public var initialState: State @@ -41,8 +41,13 @@ public final class NotificationSettingReactor: Reactor { private let checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase private let updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase - init(checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase, updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase) { - self.initialState = .init() + init(checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase, updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase, + isAgreeEventNotification: Bool, + isAgreeNoticeNotification: Bool, + isAgreePatchNoteNotification: Bool + + ) { + self.initialState = .init(isAgreeEventNotification: isAgreeEventNotification, isAgreeNoticeNotification: isAgreeNoticeNotification, isAgreePatchNoteNotification: isAgreePatchNoteNotification) self.checkNotificationPermissionUseCase = checkNotificationPermissionUseCase self.updateNotificationAgreementUseCase = updateNotificationAgreementUseCase } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift index d6114dbf..1b936661 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift @@ -9,8 +9,9 @@ import RxSwift final class NotificationSettingViewController: BaseViewController, View, UNUserNotificationCenterDelegate { typealias Reactor = NotificationSettingReactor + public var disposeBag = DisposeBag() + // MARK: - Properties - var disposeBag = DisposeBag() // MARK: - UI Components private let mainView = NotificationSettingView() diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageReactor.swift index 4774a212..49be8575 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageReactor.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageReactor.swift @@ -16,7 +16,7 @@ public final class SelectImageReactor: Reactor { public enum Action { case cancelButtonTapped case applyButtonTapped - case imageTapped(MapleIllustration) + case imageTapped(Int) } public enum Mutation { @@ -61,7 +61,8 @@ public final class SelectImageReactor: Reactor { guard let url = currentState.selectedImage?.url else { return .empty() } return updateProfileImageUseCase.execute(url: url) .andThen(.just(.navigateTo(route: .dismissWithSave))) - case .imageTapped(let image): + case .imageTapped(let index): + let image = currentState.images[index] return .just(.selectImage(image)) } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageViewContoller.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageViewContoller.swift index 0571acc7..23938438 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageViewContoller.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SelectImage/SelectImageViewContoller.swift @@ -11,11 +11,11 @@ import RxSwift import SnapKit public final class SelectImageViewContoller: BaseViewController, ModalPresentable, View { + // 수정필요 public var modalHeight: CGFloat? = 16 + 32 + UIScreen.main.bounds.size.width + 4 + 24 + 54 + 4 public typealias Reactor = SelectImageReactor - // MARK: - Properties public var disposeBag = DisposeBag() // MARK: - Components @@ -73,6 +73,11 @@ extension SelectImageViewContoller { .map { Reactor.Action.cancelButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) + + mainView.applyButton.rx.tap + .map { Reactor.Action.applyButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) } func bindViewState(reactor: Reactor) { @@ -80,6 +85,7 @@ extension SelectImageViewContoller { .take(1) .flatMapLatest { _ in reactor.pulse(\.$route) } .withUnretained(self) + .observe(on: MainScheduler.instance) .subscribe { owner, route in switch route { case .dismiss: @@ -106,4 +112,9 @@ extension SelectImageViewContoller: UICollectionViewDelegate, UICollectionViewDa cell.inject(input: SelectImageCell.Input(type: reactor.currentState.images[indexPath.row])) return cell } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let reactor = reactor else { return } + reactor.action.onNext(.imageTapped(indexPath.row)) + } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetCharacter/SetCharacterViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetCharacter/SetCharacterViewController.swift index 2945fc77..f9f22874 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetCharacter/SetCharacterViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetCharacter/SetCharacterViewController.swift @@ -13,9 +13,10 @@ public class SetCharacterViewController: BaseViewController, View { // MARK: - Properties public typealias Reactor = SetCharacterReactor - // MARK: - Components public var disposeBag = DisposeBag() + // MARK: - Components + private var mainView = SetCharacterView() } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileFactoryImpl.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileFactoryImpl.swift index acf6f55d..dfaac96f 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileFactoryImpl.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileFactoryImpl.swift @@ -8,18 +8,20 @@ public final class SetProfileFactoryImpl: SetProfileFactory { private let updateNickNameUseCase: UpdateNickNameUseCase private let logoutUseCase: LogoutUseCase private let withdrawUseCase: WithdrawUseCase + private let fetchProfileUseCase: FetchProfileUseCase - public init(selectImageFactory: SelectImageFactory, checkNickNameUseCase: CheckNickNameUseCase, updateNickNameUseCase: UpdateNickNameUseCase, logoutUseCase: LogoutUseCase, withdrawUseCase: WithdrawUseCase) { + public init(selectImageFactory: SelectImageFactory, checkNickNameUseCase: CheckNickNameUseCase, updateNickNameUseCase: UpdateNickNameUseCase, logoutUseCase: LogoutUseCase, withdrawUseCase: WithdrawUseCase, fetchProfileUseCase: FetchProfileUseCase) { self.selectImageFactory = selectImageFactory self.checkNickNameUseCase = checkNickNameUseCase self.updateNickNameUseCase = updateNickNameUseCase self.logoutUseCase = logoutUseCase self.withdrawUseCase = withdrawUseCase + self.fetchProfileUseCase = fetchProfileUseCase } public func make() -> BaseViewController { let viewController = SetProfileViewController(selectImageFactory: selectImageFactory) - viewController.reactor = SetProfileReactor(checkNickNameUseCase: checkNickNameUseCase, updateNickNameUseCase: updateNickNameUseCase, logoutUseCase: logoutUseCase, withdrawUseCase: withdrawUseCase) + viewController.reactor = SetProfileReactor(checkNickNameUseCase: checkNickNameUseCase, updateNickNameUseCase: updateNickNameUseCase, logoutUseCase: logoutUseCase, withdrawUseCase: withdrawUseCase, fetchProfileUseCase: fetchProfileUseCase) viewController.isBottomTabbarHidden = true return viewController } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift index 0ca02bda..0f91357b 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift @@ -15,6 +15,7 @@ public final class SetProfileReactor: Reactor { // MARK: - Action public enum Action { + case viewWillAppear case backButtonTapped case editButtonTapped case logoutButtonTapped @@ -30,6 +31,7 @@ public final class SetProfileReactor: Reactor { public enum Mutation { case toNavigate(Route) case setNickName(String) + case setProfile(MyPageResponse?) case showError(Bool) case beginSetText(Bool) case beginEditting @@ -41,9 +43,9 @@ public final class SetProfileReactor: Reactor { public struct State { @Pulse var route: Route = .none var setProfileState: SetProfileView.SetProfileState - var nickName: String = "" var isShowError = false var isEditingNickName = false + var profile: MyPageResponse? } // MARK: - Properties @@ -53,13 +55,15 @@ public final class SetProfileReactor: Reactor { private let updateNickNameUseCase: UpdateNickNameUseCase private let logoutUseCase: LogoutUseCase private let withdrawUseCase: WithdrawUseCase + private let fetchProfileUseCase: FetchProfileUseCase // MARK: - Init - public init(checkNickNameUseCase: CheckNickNameUseCase, updateNickNameUseCase: UpdateNickNameUseCase, logoutUseCase: LogoutUseCase, withdrawUseCase: WithdrawUseCase) { + public init(checkNickNameUseCase: CheckNickNameUseCase, updateNickNameUseCase: UpdateNickNameUseCase, logoutUseCase: LogoutUseCase, withdrawUseCase: WithdrawUseCase, fetchProfileUseCase: FetchProfileUseCase) { self.checkNickNameUseCase = checkNickNameUseCase self.updateNickNameUseCase = updateNickNameUseCase self.logoutUseCase = logoutUseCase self.withdrawUseCase = withdrawUseCase + self.fetchProfileUseCase = fetchProfileUseCase } // MARK: - Mutate @@ -85,8 +89,14 @@ public final class SetProfileReactor: Reactor { case .editButtonTapped: switch currentState.setProfileState { case .edit: - return updateNickNameUseCase.execute(nickName: currentState.nickName) - .andThen(Observable.just(.completeEditting)) + guard let profile = currentState.profile else { return .empty() } + return updateNickNameUseCase.execute(nickName: profile.nickname) + .flatMap { profile in + Observable.concat([ + .just(.setProfile(profile)), + .just(.completeEditting) + ]) + } case .normal: return .just(.beginEditting) } @@ -100,6 +110,9 @@ public final class SetProfileReactor: Reactor { case .withdraw: return withdrawUseCase.execute() .andThen(.empty()) + case .viewWillAppear: + return fetchProfileUseCase.execute() + .map { Mutation.setProfile($0)} } } @@ -110,8 +123,6 @@ public final class SetProfileReactor: Reactor { switch mutation { case .toNavigate(let route): newState.route = route - case .setNickName(let nickName): - newState.nickName = nickName case .showError(let error): newState.isShowError = error case .beginSetText(let isEditing): @@ -122,6 +133,10 @@ public final class SetProfileReactor: Reactor { newState.setProfileState = .edit case .completeEditting: newState.route = .dismissWithUpdate + case .setProfile(let profile): + newState.profile = profile + case .setNickName(let nickName): + newState.profile?.nickname = nickName } return newState diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift index e14464d4..42ed9c89 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift @@ -1,5 +1,6 @@ import UIKit +import BaseFeature import DesignSystem import DomainInterface @@ -381,8 +382,10 @@ public extension SetProfileView { } } - func setImage(image: UIImage) { - imageView.image = image + func setImage(imageUrl: String) { + ImageLoader.shared.loadImage(stringURL: imageUrl) { [weak self] image in + self?.imageView.image = image + } } func setName(name: String) { diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift index 6f96cd8d..bc29b5ac 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift @@ -18,6 +18,7 @@ public final class SetProfileViewController: BaseViewController, View { // MARK: - Properties public var disposeBag = DisposeBag() + var didReturn = PublishRelay() private var selectImageFactory: SelectImageFactory @@ -33,9 +34,6 @@ public final class SetProfileViewController: BaseViewController, View { public init(selectImageFactory: SelectImageFactory) { self.selectImageFactory = selectImageFactory super.init() - mainView.setName(name: "익명의 오무라이스케챱") - mainView.setImage(image: .add) - mainView.setPlatform(platform: .kakao) } @available(*, unavailable) @@ -78,6 +76,11 @@ extension SetProfileViewController { } private func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + mainView.backButton.rx.tap .map { Reactor.Action.backButtonTapped } .bind(to: reactor.action) @@ -119,6 +122,7 @@ extension SetProfileViewController { .map(\.setProfileState) .distinctUntilChanged() .withUnretained(self) + .observe(on: MainScheduler.instance) .bind(onNext: { owner, state in owner.view.backgroundColor = state == .edit ? .whiteMLS : .neutral100 owner.mainView.setCountHidden(state: state) @@ -126,13 +130,26 @@ extension SetProfileViewController { }) .disposed(by: disposeBag) + reactor.state + .compactMap(\.profile) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, profile in + owner.mainView.setName(name: profile.nickname) + owner.mainView.setImage(imageUrl: profile.profileUrl) + owner.mainView.setPlatform(platform: profile.platform) + }) + .disposed(by: disposeBag) + reactor.state .filter(\.isEditingNickName) - .map(\.nickName) + .compactMap(\.profile?.nickname) .distinctUntilChanged() .withUnretained(self) - .bind(onNext: { owner, nickName in - owner.mainView.setCount(count: nickName.count) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, nickname in + owner.mainView.setCount(count: nickname.count) }) .disposed(by: disposeBag) @@ -140,6 +157,7 @@ extension SetProfileViewController { .map(\.isShowError) .distinctUntilChanged() .withUnretained(self) + .observe(on: MainScheduler.instance) .bind(onNext: { owner, isShowError in owner.mainView.setError(isError: isShowError) }) @@ -149,10 +167,21 @@ extension SetProfileViewController { .take(1) .flatMapLatest { _ in reactor.pulse(\.$route) } .withUnretained(self) + .observe(on: MainScheduler.instance) .subscribe { owner, route in switch route { 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) + } + owner.presentModal(viewController) case .dismiss: owner.didReturn.accept(false) diff --git a/MLS/Presentation/MyPageFeature/MyPageFeatureDemo/AppDelegate.swift b/MLS/Presentation/MyPageFeature/MyPageFeatureDemo/AppDelegate.swift index d2ce7a01..2147726d 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeatureDemo/AppDelegate.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeatureDemo/AppDelegate.swift @@ -1,3 +1,6 @@ +// swiftlint:disable function_body_length +// swiftlint:disable line_length + import UIKit import AuthFeatureInterface diff --git a/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/NotificationSettingFactory.swift b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/NotificationSettingFactory.swift index 7d7a9c51..45a4e762 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/NotificationSettingFactory.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/NotificationSettingFactory.swift @@ -1,5 +1,5 @@ import BaseFeature public protocol NotificationSettingFactory { - func make() -> BaseViewController + func make(isAgreeEventNotification: Bool, isAgreeNoticeNotification: Bool, isAgreePatchNoteNotification: Bool) -> BaseViewController }