diff --git a/Projects/App/Sources/Application/AppComponent+Auth.swift b/Projects/App/Sources/Application/AppComponent+Auth.swift index e8b598021..e499b47ed 100644 --- a/Projects/App/Sources/Application/AppComponent+Auth.swift +++ b/Projects/App/Sources/Application/AppComponent+Auth.swift @@ -4,6 +4,7 @@ import BaseFeature import SignInFeature import SignInFeatureInterface import StorageFeature +import StorageFeatureInterface // MARK: 변수명 주의 // AppComponent 내 변수 == Dependency 내 변수 이름 같아야함 @@ -13,14 +14,10 @@ public extension AppComponent { SignInComponent(parent: self) } - var storageComponent: StorageComponent { + var storageFactory: any StorageFactory { StorageComponent(parent: self) } - var afterLoginComponent: AfterLoginComponent { - AfterLoginComponent(parent: self) - } - var requestComponent: RequestComponent { RequestComponent(parent: self) } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 5aee5b3c8..5384a273e 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -39,6 +39,7 @@ import SignInFeatureInterface import SongsDomain import SongsDomainInterface import StorageFeature +import StorageFeatureInterface import UIKit import UserDomain import UserDomainInterface @@ -177,8 +178,8 @@ private class MainTabBarDependencycd05b79389a6a7a6c20fProvider: MainTabBarDepend var artistComponent: ArtistComponent { return appComponent.artistComponent } - var storageComponent: StorageComponent { - return appComponent.storageComponent + var storageFactory: any StorageFactory { + return appComponent.storageFactory } var myInfoComponent: MyInfoComponent { return appComponent.myInfoComponent @@ -325,8 +326,17 @@ private class StorageDependency1447167c38e97ef97427Provider: StorageDependency { var signInFactory: any SignInFactory { return appComponent.signInFactory } - var afterLoginComponent: AfterLoginComponent { - return appComponent.afterLoginComponent + var myPlayListComponent: MyPlayListComponent { + return appComponent.myPlayListComponent + } + var multiPurposePopUpFactory: any MultiPurposePopUpFactory { + return appComponent.multiPurposePopUpFactory + } + var favoriteComponent: FavoriteComponent { + return appComponent.favoriteComponent + } + var textPopUpFactory: any TextPopUpFactory { + return appComponent.textPopUpFactory } private let appComponent: AppComponent init(appComponent: AppComponent) { @@ -391,6 +401,9 @@ private class MyPlayListDependency067bbf42b28f80e413acProvider: MyPlayListDepend var textPopUpFactory: any TextPopUpFactory { return appComponent.textPopUpFactory } + var signInFactory: any SignInFactory { + return appComponent.signInFactory + } private let appComponent: AppComponent init(appComponent: AppComponent) { self.appComponent = appComponent @@ -400,40 +413,6 @@ private class MyPlayListDependency067bbf42b28f80e413acProvider: MyPlayListDepend private func factory51a57a92f76af93a9ec2f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return MyPlayListDependency067bbf42b28f80e413acProvider(appComponent: parent1(component) as! AppComponent) } -private class AfterLoginDependencya880b76858e0a77ed700Provider: AfterLoginDependency { - var fetchUserInfoUseCase: any FetchUserInfoUseCase { - return appComponent.fetchUserInfoUseCase - } - var logoutUseCase: any LogoutUseCase { - return appComponent.logoutUseCase - } - var requestComponent: RequestComponent { - return appComponent.requestComponent - } - var profilePopComponent: ProfilePopComponent { - return appComponent.profilePopComponent - } - var myPlayListComponent: MyPlayListComponent { - return appComponent.myPlayListComponent - } - var multiPurposePopUpFactory: any MultiPurposePopUpFactory { - return appComponent.multiPurposePopUpFactory - } - var favoriteComponent: FavoriteComponent { - return appComponent.favoriteComponent - } - var textPopUpFactory: any TextPopUpFactory { - return appComponent.textPopUpFactory - } - private let appComponent: AppComponent - init(appComponent: AppComponent) { - self.appComponent = appComponent - } -} -/// ^->AppComponent->AfterLoginComponent -private func factory6cc9c8141e04494113b8f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { - return AfterLoginDependencya880b76858e0a77ed700Provider(appComponent: parent1(component) as! AppComponent) -} private class FavoriteDependency8f7fd37aeb6f0e5d0e30Provider: FavoriteDependency { var containSongsFactory: any ContainSongsFactory { return appComponent.containSongsFactory @@ -798,8 +777,7 @@ extension AppComponent: Registration { localTable["fetchLyricsUseCase-any FetchLyricsUseCase"] = { [unowned self] in self.fetchLyricsUseCase as Any } localTable["fetchNewSongsUseCase-any FetchNewSongsUseCase"] = { [unowned self] in self.fetchNewSongsUseCase as Any } localTable["signInFactory-any SignInFactory"] = { [unowned self] in self.signInFactory as Any } - localTable["storageComponent-StorageComponent"] = { [unowned self] in self.storageComponent as Any } - localTable["afterLoginComponent-AfterLoginComponent"] = { [unowned self] in self.afterLoginComponent as Any } + localTable["storageFactory-any StorageFactory"] = { [unowned self] in self.storageFactory as Any } localTable["requestComponent-RequestComponent"] = { [unowned self] in self.requestComponent as Any } localTable["localAuthDataSource-any LocalAuthDataSource"] = { [unowned self] in self.localAuthDataSource as Any } localTable["remoteAuthDataSource-any RemoteAuthDataSource"] = { [unowned self] in self.remoteAuthDataSource as Any } @@ -930,7 +908,7 @@ extension MainTabBarComponent: Registration { keyPathToName[\MainTabBarDependency.chartComponent] = "chartComponent-ChartComponent" keyPathToName[\MainTabBarDependency.searchFactory] = "searchFactory-any SearchFactory" keyPathToName[\MainTabBarDependency.artistComponent] = "artistComponent-ArtistComponent" - keyPathToName[\MainTabBarDependency.storageComponent] = "storageComponent-StorageComponent" + keyPathToName[\MainTabBarDependency.storageFactory] = "storageFactory-any StorageFactory" keyPathToName[\MainTabBarDependency.myInfoComponent] = "myInfoComponent-MyInfoComponent" keyPathToName[\MainTabBarDependency.noticePopupComponent] = "noticePopupComponent-NoticePopupComponent" keyPathToName[\MainTabBarDependency.noticeComponent] = "noticeComponent-NoticeComponent" @@ -986,7 +964,10 @@ extension ServiceInfoComponent: Registration { extension StorageComponent: Registration { public func registerItems() { keyPathToName[\StorageDependency.signInFactory] = "signInFactory-any SignInFactory" - keyPathToName[\StorageDependency.afterLoginComponent] = "afterLoginComponent-AfterLoginComponent" + keyPathToName[\StorageDependency.myPlayListComponent] = "myPlayListComponent-MyPlayListComponent" + keyPathToName[\StorageDependency.multiPurposePopUpFactory] = "multiPurposePopUpFactory-any MultiPurposePopUpFactory" + keyPathToName[\StorageDependency.favoriteComponent] = "favoriteComponent-FavoriteComponent" + keyPathToName[\StorageDependency.textPopUpFactory] = "textPopUpFactory-any TextPopUpFactory" } } extension FaqComponent: Registration { @@ -1010,18 +991,7 @@ extension MyPlayListComponent: Registration { keyPathToName[\MyPlayListDependency.deletePlayListUseCase] = "deletePlayListUseCase-any DeletePlayListUseCase" keyPathToName[\MyPlayListDependency.logoutUseCase] = "logoutUseCase-any LogoutUseCase" keyPathToName[\MyPlayListDependency.textPopUpFactory] = "textPopUpFactory-any TextPopUpFactory" - } -} -extension AfterLoginComponent: Registration { - public func registerItems() { - keyPathToName[\AfterLoginDependency.fetchUserInfoUseCase] = "fetchUserInfoUseCase-any FetchUserInfoUseCase" - keyPathToName[\AfterLoginDependency.logoutUseCase] = "logoutUseCase-any LogoutUseCase" - keyPathToName[\AfterLoginDependency.requestComponent] = "requestComponent-RequestComponent" - keyPathToName[\AfterLoginDependency.profilePopComponent] = "profilePopComponent-ProfilePopComponent" - keyPathToName[\AfterLoginDependency.myPlayListComponent] = "myPlayListComponent-MyPlayListComponent" - keyPathToName[\AfterLoginDependency.multiPurposePopUpFactory] = "multiPurposePopUpFactory-any MultiPurposePopUpFactory" - keyPathToName[\AfterLoginDependency.favoriteComponent] = "favoriteComponent-FavoriteComponent" - keyPathToName[\AfterLoginDependency.textPopUpFactory] = "textPopUpFactory-any TextPopUpFactory" + keyPathToName[\MyPlayListDependency.signInFactory] = "signInFactory-any SignInFactory" } } extension FavoriteComponent: Registration { @@ -1205,7 +1175,6 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi registerProviderFactory("^->AppComponent->FaqComponent", factory4e13cc6545633ffc2ed5f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->QuestionComponent", factoryedad1813a36115eec11ef47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->MyPlayListComponent", factory51a57a92f76af93a9ec2f47b58f8f304c97af4d5) - registerProviderFactory("^->AppComponent->AfterLoginComponent", factory6cc9c8141e04494113b8f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->FavoriteComponent", factory8e4acb90bd0d9b48604af47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->RequestComponent", factory13954fb3ec537bab80bcf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->NoticeDetailComponent", factory3db143c2f80d621d5a7fe3b0c44298fc1c149afb) diff --git a/Projects/Features/BaseFeature/Resources/Warning.xib b/Projects/Features/BaseFeature/Resources/Warning.xib index 6f6963fba..7476ccf19 100644 --- a/Projects/Features/BaseFeature/Resources/Warning.xib +++ b/Projects/Features/BaseFeature/Resources/Warning.xib @@ -1,8 +1,9 @@ - + - + + diff --git a/Projects/Features/MainTabFeature/Sources/Components/MainTabBarComponent.swift b/Projects/Features/MainTabFeature/Sources/Components/MainTabBarComponent.swift index 7820ba211..34af7b684 100644 --- a/Projects/Features/MainTabFeature/Sources/Components/MainTabBarComponent.swift +++ b/Projects/Features/MainTabFeature/Sources/Components/MainTabBarComponent.swift @@ -9,6 +9,7 @@ import NoticeDomainInterface import SearchFeature import SearchFeatureInterface import StorageFeature +import StorageFeatureInterface public protocol MainTabBarDependency: Dependency { var fetchNoticeUseCase: any FetchNoticeUseCase { get } @@ -16,7 +17,7 @@ public protocol MainTabBarDependency: Dependency { var chartComponent: ChartComponent { get } var searchFactory: any SearchFactory { get } var artistComponent: ArtistComponent { get } - var storageComponent: StorageComponent { get } + var storageFactory: any StorageFactory { get } var myInfoComponent: MyInfoComponent { get } var noticePopupComponent: NoticePopupComponent { get } var noticeComponent: NoticeComponent { get } @@ -33,7 +34,7 @@ public final class MainTabBarComponent: Component { chartComponent: self.dependency.chartComponent, searchFactory: self.dependency.searchFactory, artistComponent: self.dependency.artistComponent, - storageCompoent: self.dependency.storageComponent, + storageFactory: self.dependency.storageFactory, myInfoComponent: self.dependency.myInfoComponent, noticePopupComponent: self.dependency.noticePopupComponent, noticeComponent: self.dependency.noticeComponent, diff --git a/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift b/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift index 601739622..0aa0073c2 100644 --- a/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift +++ b/Projects/Features/MainTabFeature/Sources/ViewControllers/MainTabBarViewController.swift @@ -11,6 +11,7 @@ import SearchFeature import SearchFeatureInterface import SnapKit import StorageFeature +import StorageFeatureInterface import UIKit import Utility @@ -23,7 +24,7 @@ public final class MainTabBarViewController: BaseViewController, ViewControllerF chartComponent.makeView().wrapNavigationController, searchFactory.makeView().wrapNavigationController, artistComponent.makeView().wrapNavigationController, - storageComponent.makeView().wrapNavigationController, + storageFactory.makeView().wrapNavigationController, myInfoComponent.makeView().wrapNavigationController ] }() @@ -37,7 +38,7 @@ public final class MainTabBarViewController: BaseViewController, ViewControllerF private var chartComponent: ChartComponent! private var searchFactory: SearchFactory! private var artistComponent: ArtistComponent! - private var storageComponent: StorageComponent! + private var storageFactory: StorageFactory! private var myInfoComponent: MyInfoComponent! private var noticePopupComponent: NoticePopupComponent! private var noticeComponent: NoticeComponent! @@ -65,7 +66,7 @@ public final class MainTabBarViewController: BaseViewController, ViewControllerF chartComponent: ChartComponent, searchFactory: SearchFactory, artistComponent: ArtistComponent, - storageCompoent: StorageComponent, + storageFactory: StorageFactory, myInfoComponent: MyInfoComponent, noticePopupComponent: NoticePopupComponent, noticeComponent: NoticeComponent, @@ -77,7 +78,7 @@ public final class MainTabBarViewController: BaseViewController, ViewControllerF viewController.chartComponent = chartComponent viewController.searchFactory = searchFactory viewController.artistComponent = artistComponent - viewController.storageComponent = storageCompoent + viewController.storageFactory = storageFactory viewController.myInfoComponent = myInfoComponent viewController.noticePopupComponent = noticePopupComponent viewController.noticeComponent = noticeComponent diff --git a/Projects/Features/PlaylistFeature/Sources/Reactors/PlaylistDetailReactor.swift b/Projects/Features/PlaylistFeature/Sources/Reactors/PlaylistDetailReactor.swift index a1ea3fa06..1d24dbdbb 100644 --- a/Projects/Features/PlaylistFeature/Sources/Reactors/PlaylistDetailReactor.swift +++ b/Projects/Features/PlaylistFeature/Sources/Reactors/PlaylistDetailReactor.swift @@ -189,7 +189,10 @@ private extension PlaylistDetailReactor { /// 순서 변경 func updateOrder(src: Int, dest: Int) -> Observable { - var tmp = (currentState.dataSource.first ?? PlayListDetailSectionModel(model: 0, items: [])).items + guard var tmp = currentState.dataSource.first?.items else { + LogManager.printError("playlist detail datasource is empty") + return .empty() + } let target = tmp[src] tmp.remove(at: src) tmp.insert(target, at: dest) diff --git a/Projects/Features/SignInFeature/Sources/ViewControllers/LoginViewController.swift b/Projects/Features/SignInFeature/Sources/ViewControllers/LoginViewController.swift index d5d1b4477..364016514 100644 --- a/Projects/Features/SignInFeature/Sources/ViewControllers/LoginViewController.swift +++ b/Projects/Features/SignInFeature/Sources/ViewControllers/LoginViewController.swift @@ -106,6 +106,108 @@ extension LoginViewController { } } +extension LoginViewController { + public func configureUI() { + appLogoImageView.image = DesignSystemAsset.Logo.applogo.image + scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 56, right: 0) + configureOAuthLogin() + configureService() + } + + private func configureOAuthLogin() { + let loginAttributedString: [NSMutableAttributedString] = [ + NSMutableAttributedString.init(string: "네이버로 로그인하기"), + NSMutableAttributedString.init(string: "구글로 로그인하기"), + NSMutableAttributedString.init(string: "애플로 로그인하기") + ] + + for attr in loginAttributedString { + attr.addAttributes( + [ + .font: DesignSystemFontFamily.Pretendard.medium.font(size: 16), + .foregroundColor: DesignSystemAsset.GrayColor.gray900.color, + .kern: -0.5 + ], + range: NSRange(location: 0, length: attr.string.count) + ) + } + + let superViewArr: [UIView] = [naverSuperView, googleSuperView, appleSuperView] + for sv in superViewArr { + sv.backgroundColor = .white.withAlphaComponent(0.4) + sv.layer.cornerRadius = 12 + sv.layer.borderColor = DesignSystemAsset.GrayColor.gray200.color.cgColor + sv.layer.borderWidth = 1 + } + + naverImageVIew.image = DesignSystemAsset.Signup.naver.image + naverLoginButton.setAttributedTitle(loginAttributedString[0], for: .normal) + googleImageView.image = DesignSystemAsset.Signup.google.image + googleLoginButton.setAttributedTitle(loginAttributedString[1], for: .normal) + appleImageView.image = DesignSystemAsset.Signup.apple.image + appleLoginButton.setAttributedTitle(loginAttributedString[2], for: .normal) + } + + private func configureService() { + let appAttributedString = NSMutableAttributedString + .init(string: "왁타버스 뮤직") + + appAttributedString.addAttributes( + [ + .font: DesignSystemFontFamily.Pretendard.medium.font(size: 20), + .foregroundColor: DesignSystemAsset.GrayColor.gray900.color, + .kern: -0.5 + ], + range: NSRange(location: 0, length: appAttributedString.string.count) + ) + appNameLabel.attributedText = appAttributedString + + let descriptionAttributedString = NSMutableAttributedString + .init(string: "페이지를 이용하기 위해 로그인이 필요합니다.") + + descriptionAttributedString.addAttributes( + [ + .font: DesignSystemFontFamily.Pretendard.light.font(size: 14), + .foregroundColor: DesignSystemAsset.GrayColor.gray600.color, + .kern: -0.5 + ], + range: NSRange(location: 0, length: descriptionAttributedString.string.count) + ) + descriptionLabel.attributedText = descriptionAttributedString + + let servicePrivacyButtons: [UIButton] = [serviceButton, privacyButton] + let termsAttributedString: [NSMutableAttributedString] = [ + NSMutableAttributedString.init(string: "서비스 이용약관"), + NSMutableAttributedString.init(string: "개인정보처리방침") + ] + for attr in termsAttributedString { + attr.addAttributes( + [ + .font: DesignSystemFontFamily.Pretendard.medium.font(size: 14), + .foregroundColor: DesignSystemAsset.GrayColor.gray600.color, + .kern: -0.5 + ], + range: NSRange(location: 0, length: attr.string.count) + ) + } + servicePrivacyButtons[0].setAttributedTitle(termsAttributedString[0], for: .normal) + servicePrivacyButtons[1].setAttributedTitle(termsAttributedString[1], for: .normal) + + for btn in servicePrivacyButtons { + btn.layer.cornerRadius = 8 + btn.layer.borderColor = DesignSystemAsset.GrayColor.gray300.color.cgColor + btn.layer.borderWidth = 1 + btn.clipsToBounds = true + } + + versionLabel.text = "버전 정보 " + APP_VERSION() + versionLabel.textColor = DesignSystemAsset.GrayColor.gray400.color + versionLabel.font = DesignSystemFontFamily.Pretendard.light.font(size: 12) + versionLabel.setTextWithAttributes(kernValue: -0.5) + versionLabel.textAlignment = .center + } +} + public extension LoginViewController { func scrollToTop() { scrollView.setContentOffset(.zero, animated: true) diff --git a/Projects/Features/SignInFeature/Sources/Views/LoginUIExtension.swift b/Projects/Features/SignInFeature/Sources/Views/LoginUIExtension.swift deleted file mode 100644 index ab28beb1e..000000000 --- a/Projects/Features/SignInFeature/Sources/Views/LoginUIExtension.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// LoginView + UI.swift -// SignInFeature -// -// Created by 김대희 on 2023/03/13. -// Copyright © 2023 yongbeomkwak. All rights reserved. -// - -import DesignSystem -import UIKit -import Utility - -extension LoginViewController { - public func configureUI() { - appLogoImageView.image = DesignSystemAsset.Logo.applogo.image - scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 56, right: 0) - configureOAuthLogin() - configureService() - } - - private func configureOAuthLogin() { - let loginAttributedString: [NSMutableAttributedString] = [ - NSMutableAttributedString.init(string: "네이버로 로그인하기"), - NSMutableAttributedString.init(string: "구글로 로그인하기"), - NSMutableAttributedString.init(string: "애플로 로그인하기") - ] - - for attr in loginAttributedString { - attr.addAttributes( - [ - .font: DesignSystemFontFamily.Pretendard.medium.font(size: 16), - .foregroundColor: DesignSystemAsset.GrayColor.gray900.color, - .kern: -0.5 - ], - range: NSRange(location: 0, length: attr.string.count) - ) - } - - let superViewArr: [UIView] = [naverSuperView, googleSuperView, appleSuperView] - for sv in superViewArr { - sv.backgroundColor = .white.withAlphaComponent(0.4) - sv.layer.cornerRadius = 12 - sv.layer.borderColor = DesignSystemAsset.GrayColor.gray200.color.cgColor - sv.layer.borderWidth = 1 - } - - naverImageVIew.image = DesignSystemAsset.Signup.naver.image - naverLoginButton.setAttributedTitle(loginAttributedString[0], for: .normal) - googleImageView.image = DesignSystemAsset.Signup.google.image - googleLoginButton.setAttributedTitle(loginAttributedString[1], for: .normal) - appleImageView.image = DesignSystemAsset.Signup.apple.image - appleLoginButton.setAttributedTitle(loginAttributedString[2], for: .normal) - } - - private func configureService() { - let appAttributedString = NSMutableAttributedString - .init(string: "왁타버스 뮤직") - - appAttributedString.addAttributes( - [ - .font: DesignSystemFontFamily.Pretendard.medium.font(size: 20), - .foregroundColor: DesignSystemAsset.GrayColor.gray900.color, - .kern: -0.5 - ], - range: NSRange(location: 0, length: appAttributedString.string.count) - ) - appNameLabel.attributedText = appAttributedString - - let descriptionAttributedString = NSMutableAttributedString - .init(string: "페이지를 이용하기 위해 로그인이 필요합니다.") - - descriptionAttributedString.addAttributes( - [ - .font: DesignSystemFontFamily.Pretendard.light.font(size: 14), - .foregroundColor: DesignSystemAsset.GrayColor.gray600.color, - .kern: -0.5 - ], - range: NSRange(location: 0, length: descriptionAttributedString.string.count) - ) - descriptionLabel.attributedText = descriptionAttributedString - - let servicePrivacyButtons: [UIButton] = [serviceButton, privacyButton] - let termsAttributedString: [NSMutableAttributedString] = [ - NSMutableAttributedString.init(string: "서비스 이용약관"), - NSMutableAttributedString.init(string: "개인정보처리방침") - ] - for attr in termsAttributedString { - attr.addAttributes( - [ - .font: DesignSystemFontFamily.Pretendard.medium.font(size: 14), - .foregroundColor: DesignSystemAsset.GrayColor.gray600.color, - .kern: -0.5 - ], - range: NSRange(location: 0, length: attr.string.count) - ) - } - servicePrivacyButtons[0].setAttributedTitle(termsAttributedString[0], for: .normal) - servicePrivacyButtons[1].setAttributedTitle(termsAttributedString[1], for: .normal) - - for btn in servicePrivacyButtons { - btn.layer.cornerRadius = 8 - btn.layer.borderColor = DesignSystemAsset.GrayColor.gray300.color.cgColor - btn.layer.borderWidth = 1 - btn.clipsToBounds = true - } - - versionLabel.text = "버전 정보 " + APP_VERSION() - versionLabel.textColor = DesignSystemAsset.GrayColor.gray400.color - versionLabel.font = DesignSystemFontFamily.Pretendard.light.font(size: 12) - versionLabel.setTextWithAttributes(kernValue: -0.5) - versionLabel.textAlignment = .center - } -} diff --git a/Projects/Features/StorageFeature/Project.swift b/Projects/Features/StorageFeature/Project.swift index 2e932abba..6da944f2d 100644 --- a/Projects/Features/StorageFeature/Project.swift +++ b/Projects/Features/StorageFeature/Project.swift @@ -5,6 +5,7 @@ import ProjectDescriptionHelpers let project = Project.module( name: ModulePaths.Feature.StorageFeature.rawValue, targets: [ + .interface(module: .feature(.StorageFeature)), .implements( module: .feature(.StorageFeature), product: .staticFramework, @@ -13,10 +14,11 @@ let project = Project.module( dependencies: [ .feature(target: .SignInFeature), .feature(target: .PlaylistFeature, type: .interface), + .feature(target: .StorageFeature, type: .interface), .domain(target: .FaqDomain, type: .interface), .domain(target: .NoticeDomain, type: .interface), .domain(target: .PlayListDomain, type: .interface), - .domain(target: .UserDomain, type: .interface) + .domain(target: .UserDomain, type: .interface), ] ) ) diff --git a/Projects/Features/StorageFeature/Resources/Storage.storyboard b/Projects/Features/StorageFeature/Resources/Storage.storyboard index cfca2d1c1..89eb20950 100644 --- a/Projects/Features/StorageFeature/Resources/Storage.storyboard +++ b/Projects/Features/StorageFeature/Resources/Storage.storyboard @@ -1,9 +1,9 @@ - + - + @@ -11,42 +11,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -65,6 +33,14 @@ + @@ -81,113 +57,28 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - + - - - - - - + @@ -1947,13 +1838,13 @@ - + - + - + diff --git a/Projects/Features/StorageFeature/Sources/Components/AfterLoginComponent.swift b/Projects/Features/StorageFeature/Sources/Components/AfterLoginComponent.swift deleted file mode 100644 index 0cc1daf8d..000000000 --- a/Projects/Features/StorageFeature/Sources/Components/AfterLoginComponent.swift +++ /dev/null @@ -1,34 +0,0 @@ -import AuthDomainInterface -import BaseFeature -import BaseFeatureInterface -import Foundation -import NeedleFoundation -import UserDomainInterface - -public protocol AfterLoginDependency: Dependency { - var fetchUserInfoUseCase: any FetchUserInfoUseCase { get } - var logoutUseCase: any LogoutUseCase { get } - var requestComponent: RequestComponent { get } - var profilePopComponent: ProfilePopComponent { get } - var myPlayListComponent: MyPlayListComponent { get } - var multiPurposePopUpFactory: any MultiPurposePopUpFactory { get } - var favoriteComponent: FavoriteComponent { get } - var textPopUpFactory: any TextPopUpFactory { get } -} - -public final class AfterLoginComponent: Component { - public func makeView() -> AfterLoginViewController { - return AfterLoginViewController.viewController( - viewModel: .init( - fetchUserInfoUseCase: dependency.fetchUserInfoUseCase, - logoutUseCase: dependency.logoutUseCase - ), - requestComponent: dependency.requestComponent, - profilePopComponent: dependency.profilePopComponent, - myPlayListComponent: dependency.myPlayListComponent, - multiPurposePopUpFactory: dependency.multiPurposePopUpFactory, - favoriteComponent: dependency.favoriteComponent, - textPopUpFactory: dependency.textPopUpFactory - ) - } -} diff --git a/Projects/Features/StorageFeature/Sources/Components/MyPlayListComponent.swift b/Projects/Features/StorageFeature/Sources/Components/MyPlayListComponent.swift index 65c15337b..2fb2edf94 100644 --- a/Projects/Features/StorageFeature/Sources/Components/MyPlayListComponent.swift +++ b/Projects/Features/StorageFeature/Sources/Components/MyPlayListComponent.swift @@ -1,17 +1,11 @@ -// -// SearchComponent.swift -// SearchFeature -// -// Created by yongbeomkwak on 2023/02/10. -// Copyright © 2023 yongbeomkwak. All rights reserved. -// - import AuthDomainInterface import BaseFeature import BaseFeatureInterface import Foundation import NeedleFoundation import PlaylistFeatureInterface +import SignInFeatureInterface +import UIKit import UserDomainInterface public protocol MyPlayListDependency: Dependency { @@ -22,20 +16,23 @@ public protocol MyPlayListDependency: Dependency { var deletePlayListUseCase: any DeletePlayListUseCase { get } var logoutUseCase: any LogoutUseCase { get } var textPopUpFactory: any TextPopUpFactory { get } + var signInFactory: any SignInFactory { get } } public final class MyPlayListComponent: Component { - public func makeView() -> MyPlayListViewController { + public func makeView() -> UIViewController { return MyPlayListViewController.viewController( - viewModel: .init( - fetchPlayListUseCase: dependency.fetchPlayListUseCase, - editPlayListOrderUseCase: dependency.editPlayListOrderUseCase, - deletePlayListUseCase: dependency.deletePlayListUseCase, - logoutUseCase: dependency.logoutUseCase - ), + // viewModel: .init( +// fetchPlayListUseCase: dependency.fetchPlayListUseCase, +// editPlayListOrderUseCase: dependency.editPlayListOrderUseCase, +// deletePlayListUseCase: dependency.deletePlayListUseCase, +// logoutUseCase: dependency.logoutUseCase +// ), + reactor: MyPlaylistReactor(), multiPurposePopUpFactory: dependency.multiPurposePopUpFactory, playlistDetailFactory: dependency.playlistDetailFactory, - textPopUpFactory: dependency.textPopUpFactory + textPopUpFactory: dependency.textPopUpFactory, + signInFactory: dependency.signInFactory ) } } diff --git a/Projects/Features/StorageFeature/Sources/Components/StorageComponent.swift b/Projects/Features/StorageFeature/Sources/Components/StorageComponent.swift index 044cfce36..4c0ff950d 100644 --- a/Projects/Features/StorageFeature/Sources/Components/StorageComponent.swift +++ b/Projects/Features/StorageFeature/Sources/Components/StorageComponent.swift @@ -1,17 +1,28 @@ +import BaseFeature +import BaseFeatureInterface import Foundation import NeedleFoundation import SignInFeatureInterface +import StorageFeatureInterface +import UIKit public protocol StorageDependency: Dependency { var signInFactory: any SignInFactory { get } - var afterLoginComponent: AfterLoginComponent { get } + var myPlayListComponent: MyPlayListComponent { get } + var multiPurposePopUpFactory: any MultiPurposePopUpFactory { get } + var favoriteComponent: FavoriteComponent { get } + var textPopUpFactory: any TextPopUpFactory { get } } -public final class StorageComponent: Component { - public func makeView() -> StorageViewController { +public final class StorageComponent: Component, StorageFactory { + public func makeView() -> UIViewController { return StorageViewController.viewController( - signInFactory: dependency.signInFactory, - afterLoginComponent: dependency.afterLoginComponent + reactor: StorageReactor(), + myPlayListComponent: dependency.myPlayListComponent, + multiPurposePopUpFactory: dependency.multiPurposePopUpFactory, + favoriteComponent: dependency.favoriteComponent, + textPopUpFactory: dependency.textPopUpFactory, + signInFactory: dependency.signInFactory ) } } diff --git a/Projects/Features/StorageFeature/Sources/Reactors/MyPlaylistReactor.swift b/Projects/Features/StorageFeature/Sources/Reactors/MyPlaylistReactor.swift new file mode 100644 index 000000000..526ff4c6a --- /dev/null +++ b/Projects/Features/StorageFeature/Sources/Reactors/MyPlaylistReactor.swift @@ -0,0 +1,163 @@ +import Foundation +import LogManager +import ReactorKit +import RxCocoa +import RxSwift +import UserDomainInterface + +final class MyPlaylistReactor: Reactor { + enum Action { + case viewDidLoad + case refresh + case itemMoved(ItemMovedEvent) + case tapDidEditButton + case tapDidSaveButton + case tapDidPlaylist(Int) + case tapAll(isSelecting: Bool) + } + + enum Mutation { + case updateDataSource([MyPlayListSectionModel]) + case switchEditingState(Bool) + case updateOrder([PlayListEntity]) + case changeSelectedState(data: [PlayListEntity], selectedCount: Int) + case changeAllState(data: [PlayListEntity], selectedCount: Int) + } + + struct State { + var isEditing: Bool + var dataSource: [MyPlayListSectionModel] + var backupDataSource: [MyPlayListSectionModel] + var selectedItemCount: Int + } + + var initialState: State + private let storageCommonService: any StorageCommonService + + init(storageCommonService: any StorageCommonService = DefaultStorageCommonService.shared) { + self.initialState = State( + isEditing: false, + dataSource: [], + backupDataSource: [], + selectedItemCount: 0 + ) + + self.storageCommonService = storageCommonService + } + + deinit { + LogManager.printDebug("❌ Deinit \(Self.self)") + } + + func mutate(action: Action) -> Observable { + switch action { + case .viewDidLoad: + updateDataSource() + case .refresh: + updateDataSource() + case .tapDidEditButton: + switchEditing(true) + case .tapDidSaveButton: + // TODO: USECASE 연결 + switchEditing(false) + case let .itemMoved((sourceIndex, destinationIndex)): + updateOrder(src: sourceIndex.row, dest: destinationIndex.row) + case let .tapDidPlaylist(index): + changeSelectingState(index) + case let .tapAll(isSelecting): + tapAll(isSelecting) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .updateDataSource(dataSource): + newState.dataSource = dataSource + newState.backupDataSource = dataSource + case let .switchEditingState(flag): + newState.isEditing = flag + case let .updateOrder(dataSource): + newState.dataSource = [MyPlayListSectionModel(model: 0, items: dataSource)] + case let .changeSelectedState(data: data, selectedCount: selectedCount): + newState.dataSource = [MyPlayListSectionModel(model: 0, items: data)] + newState.selectedItemCount = selectedCount + case let .changeAllState(data: data, selectedCount: selectedCount): + newState.dataSource = [MyPlayListSectionModel(model: 0, items: data)] + newState.selectedItemCount = selectedCount + } + + return newState + } + + func transform(mutation: Observable) -> Observable { + let editState = storageCommonService.isEditingState + .map { Mutation.switchEditingState($0) } + + return Observable.merge(mutation, editState) + } +} + +extension MyPlaylistReactor { + func updateDataSource() -> Observable { + return .just( + .updateDataSource( + [MyPlayListSectionModel( + model: 0, + items: [ + .init(key: "123", title: "플리1", image: "", songlist: [], image_version: 0), + .init(key: "1234", title: "플리2", image: "", songlist: [], image_version: 0), + .init(key: "1234", title: "플리3", image: "", songlist: [], image_version: 0), + .init(key: "1234", title: "플리4", image: "", songlist: [], image_version: 0) + ] + )] + ) + ) + } + + func switchEditing(_ flag: Bool) -> Observable { + return .just(.switchEditingState(flag)) + } + + /// 순서 변경 + func updateOrder(src: Int, dest: Int) -> Observable { + guard var tmp = currentState.dataSource.first?.items else { + LogManager.printError("playlist datasource is empty") + return .empty() + } + + let target = tmp[src] + tmp.remove(at: src) + tmp.insert(target, at: dest) + return .just(.updateOrder(tmp)) + } + + func changeSelectingState(_ index: Int) -> Observable { + guard var tmp = currentState.dataSource.first?.items else { + LogManager.printError("playlist datasource is empty") + return .empty() + } + + var count = currentState.selectedItemCount + let target = tmp[index] + count = target.isSelected ? count - 1 : count + 1 + tmp[index].isSelected = !tmp[index].isSelected + return .just(.changeSelectedState(data: tmp, selectedCount: count)) + } + + /// 전체 곡 선택 / 해제 + func tapAll(_ flag: Bool) -> Observable { + guard var tmp = currentState.dataSource.first?.items else { + LogManager.printError("playlist datasource is empty") + return .empty() + } + + let count = flag ? tmp.count : 0 + + for i in 0 ..< tmp.count { + tmp[i].isSelected = flag + } + return .just(.changeAllState(data: tmp, selectedCount: count)) + } +} diff --git a/Projects/Features/StorageFeature/Sources/Reactors/StorageReactor.swift b/Projects/Features/StorageFeature/Sources/Reactors/StorageReactor.swift new file mode 100644 index 000000000..b0cf81a29 --- /dev/null +++ b/Projects/Features/StorageFeature/Sources/Reactors/StorageReactor.swift @@ -0,0 +1,88 @@ +import Foundation +import ReactorKit +import Utility + +final class StorageReactor: Reactor { + enum Action { + case switchTab(Int) + case tabDidEditButton + case saveButtonTap + case showLoginAlert + } + + enum Mutation { + case switchTabIndex(Int) + case switchEditingState(Bool) + case showLoginAlert + } + + struct State { + var isEditing: Bool + var tabIndex: Int + @Pulse var showLoginAlert: Void + } + + let initialState: State + private let storageCommonService: any StorageCommonService + + init(storageCommonService: any StorageCommonService = DefaultStorageCommonService.shared) { + initialState = State( + isEditing: false, + tabIndex: 0, + showLoginAlert: () + ) + + self.storageCommonService = storageCommonService + } + + func mutate(action: Action) -> Observable { + switch action { + case let .switchTab(index): + return switchTabIndex(index) + case .tabDidEditButton: + storageCommonService.isEditingState.onNext(true) + return switchEditingState(true) + case .showLoginAlert: + return showLoginAlert() + case .saveButtonTap: + storageCommonService.isEditingState.onNext(false) + return switchEditingState(false) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .switchTabIndex(index): + newState.tabIndex = index + case let .switchEditingState(flag): + newState.isEditing = flag + case .showLoginAlert: + newState.showLoginAlert = () + } + + return newState + } + + func transform(mutation: Observable) -> Observable { + let editState = storageCommonService.isEditingState + .map { Mutation.switchEditingState($0) } + + return Observable.merge(mutation, editState) + } +} + +extension StorageReactor { + func switchTabIndex(_ index: Int) -> Observable { + return .just(.switchTabIndex(index)) + } + + func switchEditingState(_ flag: Bool) -> Observable { + return .just(.switchEditingState(flag)) + } + + func showLoginAlert() -> Observable { + return .just(.showLoginAlert) + } +} diff --git a/Projects/Features/StorageFeature/Sources/Service/StorageCommonService.swift b/Projects/Features/StorageFeature/Sources/Service/StorageCommonService.swift new file mode 100644 index 000000000..d8f2ddb11 --- /dev/null +++ b/Projects/Features/StorageFeature/Sources/Service/StorageCommonService.swift @@ -0,0 +1,12 @@ +import Foundation +import RxSwift + +protocol StorageCommonService { + var isEditingState: BehaviorSubject { get } +} + +final class DefaultStorageCommonService: StorageCommonService { + let isEditingState: BehaviorSubject = .init(value: false) + + static let shared = DefaultStorageCommonService() +} diff --git a/Projects/Features/StorageFeature/Sources/ViewControllers/AfterLoginViewController.swift b/Projects/Features/StorageFeature/Sources/ViewControllers/AfterLoginViewController.swift deleted file mode 100644 index 70a1f037b..000000000 --- a/Projects/Features/StorageFeature/Sources/ViewControllers/AfterLoginViewController.swift +++ /dev/null @@ -1,349 +0,0 @@ -import BaseFeature -import BaseFeatureInterface -import DesignSystem -import KeychainModule -import Pageboy -import PanModal -import RxSwift -import Tabman -import UIKit -import Utility - -public final class AfterLoginViewController: TabmanViewController, ViewControllerFromStoryBoard, EditSheetViewType { - @IBOutlet weak var profileLabel: UILabel! - @IBOutlet weak var logoutButton: UIButton! - @IBOutlet weak var requestButton: UIButton! - @IBOutlet weak var tabBarView: UIView! - @IBOutlet weak var editButton: UIButton! - @IBOutlet weak var profileImageView: UIImageView! - @IBOutlet weak var profileButton: UIButton! - @IBOutlet weak var headerFakeView: UIView! - @IBOutlet weak var myPlayListFakeView: UIView! - @IBOutlet weak var favoriteFakeView: UIView! - - var textPopUpFactory: TextPopUpFactory! - - public var editSheetView: EditSheetView! - public var bottomSheetView: BottomSheetView! - private var viewControllers: [UIViewController] = [UIViewController(), UIViewController()] - - var requestComponent: RequestComponent! - var profilePopComponent: ProfilePopComponent! - var myPlayListComponent: MyPlayListComponent! - var multiPurposePopUpFactory: MultiPurposePopUpFactory! - var favoriteComponent: FavoriteComponent! - - var viewModel: AfterLoginViewModel! - lazy var input = AfterLoginViewModel.Input() - lazy var output = viewModel.transform(from: input) - let disposeBag = DisposeBag() - - @IBAction func pressRequestAction(_ sender: UIButton) { - let viewController = requestComponent.makeView() - self.navigationController?.pushViewController(viewController, animated: true) - } - - @IBAction func pressLogoutAction(_ sender: UIButton) { - guard let textPopupViewController = self.textPopUpFactory.makeView( - text: "로그아웃 하시겠습니까?", - cancelButtonIsHidden: false, - allowsDragAndTapToDismiss: nil, - confirmButtonText: nil, - cancelButtonText: nil, - completion: { self.input.pressLogOut.accept(()) }, - cancelCompletion: nil - ) as? TextPopupViewController else { - return - } - - self.showPanModal(content: textPopupViewController) - } - - override public func viewDidLoad() { - super.viewDidLoad() - configureUI() - bindRx() - bindEditButtonVisable() - } - - override public func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - self.hideEditSheet() - self.profileButton.isSelected = false - } - - /// 탭맨 페이지 변경 감지 함수 - override public func pageboyViewController( - _ pageboyViewController: PageboyViewController, - didScrollToPageAt index: TabmanViewController.PageIndex, - direction: PageboyViewController.NavigationDirection, - animated: Bool - ) { - let state = EditState(isEditing: false, force: true) - - if index == 0 { - guard let vc1 = self.viewControllers[0] as? MyPlayListViewController else { - return - } - vc1.output.state.accept(state) // 이제 돌아오는 곳을 편집 전 으로 , 이게 밑에 bindEditButtonVisable() 에 연관 됨 - editButton.isHidden = (vc1.output.dataSource.value.first?.items ?? []).isEmpty - - } else { - guard let vc2 = self.viewControllers[1] as? FavoriteViewController else { - return - } - vc2.output.state.accept(state) - editButton.isHidden = (vc2.output.dataSource.value.first?.items ?? []).isEmpty - } - output.state.accept(state) - } - - public static func viewController( - viewModel: AfterLoginViewModel, - requestComponent: RequestComponent, - profilePopComponent: ProfilePopComponent, - myPlayListComponent: MyPlayListComponent, - multiPurposePopUpFactory: MultiPurposePopUpFactory, - favoriteComponent: FavoriteComponent, - textPopUpFactory: TextPopUpFactory - ) -> AfterLoginViewController { - let viewController = AfterLoginViewController.viewController(storyBoardName: "Storage", bundle: Bundle.module) - viewController.viewModel = viewModel - viewController.requestComponent = requestComponent - viewController.profilePopComponent = profilePopComponent - viewController.myPlayListComponent = myPlayListComponent - viewController.multiPurposePopUpFactory = multiPurposePopUpFactory - viewController.favoriteComponent = favoriteComponent - viewController.viewControllers = [myPlayListComponent.makeView(), favoriteComponent.makeView()] - viewController.textPopUpFactory = textPopUpFactory - return viewController - } - - deinit { - DEBUG_LOG("❌ \(Self.self)") - } -} - -extension AfterLoginViewController { - private func bindRx() { - output.state.subscribe { [weak self] state in - guard let self = self else { - return - } - - let attr = NSMutableAttributedString( - string: state.isEditing ? "완료" : "편집", - attributes: [ - .font: DesignSystemFontFamily.Pretendard.bold.font(size: 12), - .foregroundColor: state.isEditing ? DesignSystemAsset.PrimaryColor - .point.color : DesignSystemAsset.GrayColor.gray400.color - ] - ) - self.editButton.layer.borderColor = state.isEditing ? DesignSystemAsset.PrimaryColor.point.color - .cgColor : DesignSystemAsset.GrayColor.gray300.color.cgColor - self.editButton.setAttributedTitle(attr, for: .normal) - self.isScrollEnabled = !state.isEditing // 편집 시 , 옆 탭으로 swipe를 막기 위함 - self.headerFakeView.isHidden = !state.isEditing - - if state.isEditing { - self.myPlayListFakeView.isHidden = self.currentIndex == 0 - self.favoriteFakeView.isHidden = self.currentIndex == 1 - } else { - self.myPlayListFakeView.isHidden = true - self.favoriteFakeView.isHidden = true - } - }.disposed(by: disposeBag) - - editButton.rx.tap - .withLatestFrom(output.state) - .map { EditState(isEditing: !$0.isEditing, force: $0.force) } - .do(onNext: { [weak self] (state: EditState) in - guard let self = self else { - return - } - - // 프로필 편집 팝업을 띄운 상태에서 리스트 편집 시 > 프로필 편집 팝업을 제거 - if self.editSheetView != nil { - self.hideEditSheet() - } - - let nextState = EditState(isEditing: state.isEditing, force: false) - - if self.currentIndex ?? 0 == 0 { - guard let vc = self.viewControllers[0] as? MyPlayListViewController else { - return - } - vc.output.state.accept(nextState) - } else { - guard let vc = self.viewControllers[1] as? FavoriteViewController else { - return - } - vc.output.state.accept(nextState) - } - }) - .bind(to: output.state) - .disposed(by: disposeBag) - - profileButton.rx.tap.subscribe(onNext: { [weak self] in - guard let self = self else { return } - self.profileButton.isSelected = !self.profileButton.isSelected - - if self.profileButton.isSelected { - self.showEditSheet(in: self.view, type: .profile) - self.editSheetView.delegate = self - } else { - self.hideEditSheet() - } - }).disposed(by: disposeBag) - - Utility.PreferenceManager.$userInfo - .debug("$userInfo") - .filter { $0 != nil } - .subscribe(onNext: { [weak self] model in - guard let self = self, let model = model else { - return - } - self.profileLabel.text = AES256.decrypt(encoded: model.name).correctionNickName - self.profileImageView.kf.setImage( - with: URL(string: WMImageAPI.fetchProfile(name: model.profile, version: model.version).toString), - placeholder: nil, - options: [.transition(.fade(0.2))] - ) - }).disposed(by: disposeBag) - } - - func bindEditButtonVisable() { - guard let vc1 = self.viewControllers[0] as? MyPlayListViewController else { - return - } - guard let vc2 = self.viewControllers[1] as? FavoriteViewController else { - return - } - - vc1.output.dataSource - .skip(1) - .filter { [weak self] _ in - guard let self = self else { return false } - return (self.currentIndex ?? 0) == 0 - } - .map { ($0.first?.items ?? []).isEmpty } - .bind(to: editButton.rx.isHidden) - .disposed(by: disposeBag) - - vc2.output.dataSource - .skip(1) - .filter { [weak self] _ in - guard let self = self else { return false } - return (self.currentIndex ?? 0) == 1 - } - .map { ($0.first?.items ?? []).isEmpty } - .bind(to: editButton.rx.isHidden) - .disposed(by: disposeBag) - } - - private func configureUI() { - profileImageView.layer.cornerRadius = 20 - profileLabel.font = DesignSystemFontFamily.Pretendard.medium.font(size: 16) - - logoutButton.setImage(DesignSystemAsset.Storage.logout.image, for: .normal) - requestButton.setImage(DesignSystemAsset.Storage.request.image, for: .normal) - - editButton.layer.cornerRadius = 4 - editButton.layer.borderWidth = 1 - editButton.backgroundColor = .clear - editButton.isHidden = true - - myPlayListFakeView.isHidden = true - favoriteFakeView.isHidden = true - - // 탭바 설정 - self.dataSource = self - let bar = TMBar.ButtonBar() - - // 배경색 - bar.backgroundView.style = .flat(color: .clear) - - // 간격 설정 - bar.layout.contentInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) - bar.layout.contentMode = .intrinsic - bar.layout.transitionStyle = .progressive - - // 버튼 글씨 커스텀 - bar.buttons.customize { button in - button.tintColor = DesignSystemAsset.GrayColor.gray400.color - button.selectedTintColor = DesignSystemAsset.GrayColor.gray900.color - button.font = DesignSystemFontFamily.Pretendard.medium.font(size: 16) - button.selectedFont = DesignSystemFontFamily.Pretendard.bold.font(size: 16) - } - - // indicator - bar.indicator.weight = .custom(value: 3) - bar.indicator.tintColor = DesignSystemAsset.PrimaryColor.point.color - bar.indicator.overscrollBehavior = .compress - addBar(bar, dataSource: self, at: .custom(view: tabBarView, layout: nil)) - bar.layer.addBorder( - [.bottom], - color: DesignSystemAsset.GrayColor.gray300.color.withAlphaComponent(0.4), - height: 1 - ) - } -} - -extension AfterLoginViewController: EditSheetViewDelegate { - public func buttonTapped(type: EditSheetSelectType) { - switch type { - case .profile: - let profile = self.profilePopComponent.makeView() - self.showPanModal(content: profile) - case .nickname: - let nickname = self.multiPurposePopUpFactory.makeView(type: .nickname, key: "", completion: nil) - self.showEntryKitModal(content: nickname, height: 296) - default: - return - } - self.hideEditSheet() - self.profileButton.isSelected = false - } -} - -extension AfterLoginViewController: PageboyViewControllerDataSource, TMBarDataSource { - public func numberOfViewControllers(in pageboyViewController: Pageboy.PageboyViewController) -> Int { - self.viewControllers.count - } - - public func viewController( - for pageboyViewController: Pageboy.PageboyViewController, - at index: Pageboy.PageboyViewController.PageIndex - ) -> UIViewController? { - viewControllers[index] - } - - public func defaultPage(for pageboyViewController: Pageboy.PageboyViewController) -> Pageboy.PageboyViewController - .Page? { - nil - } - - public func barItem(for bar: Tabman.TMBar, at index: Int) -> Tabman.TMBarItemable { - switch index { - case 0: - return TMBarItem(title: "내 리스트") - case 1: - return TMBarItem(title: "좋아요") - default: - let title = "Page \(index)" - return TMBarItem(title: title) - } - } -} - -extension AfterLoginViewController { - func scrollToTop() { - let current: Int = self.currentIndex ?? 0 - guard self.viewControllers.count > current else { return } - if let myPlayList = self.viewControllers[current] as? MyPlayListViewController { - myPlayList.scrollToTop() - } else if let favorite = self.viewControllers[current] as? FavoriteViewController { - favorite.scrollToTop() - } - } -} diff --git a/Projects/Features/StorageFeature/Sources/ViewControllers/FavoriteViewController.swift b/Projects/Features/StorageFeature/Sources/ViewControllers/FavoriteViewController.swift index fd9d278d4..63f247700 100644 --- a/Projects/Features/StorageFeature/Sources/ViewControllers/FavoriteViewController.swift +++ b/Projects/Features/StorageFeature/Sources/ViewControllers/FavoriteViewController.swift @@ -103,15 +103,16 @@ extension FavoriteViewController { if state.isEditing == false && state.force == false { // 정상적인 편집 완료 이벤트 self.input.runEditing.onNext(()) } - guard let parent = self.parent?.parent as? AfterLoginViewController else { - return - } + // TODO: Storage 리팩 후 +// guard let parent = self.parent?.parent as? AfterLoginViewController else { +// return +// } // 탭맨 쪽 편집 변경 - let isEdit: Bool = state.isEditing - parent.output.state.accept(EditState(isEditing: isEdit, force: true)) - self.tableView.refreshControl = isEdit ? nil : self.refreshControl - self.tableView.setEditing(isEdit, animated: true) - self.tableView.reloadData() +// let isEdit: Bool = state.isEditing +// parent.output.state.accept(EditState(isEditing: isEdit, force: true)) +// self.tableView.refreshControl = isEdit ? nil : self.refreshControl +// self.tableView.setEditing(isEdit, animated: true) +// self.tableView.reloadData() }) .disposed(by: disposeBag) diff --git a/Projects/Features/StorageFeature/Sources/ViewControllers/MyPlayListViewController.swift b/Projects/Features/StorageFeature/Sources/ViewControllers/MyPlayListViewController.swift index fc309b7fe..d76bd9f5b 100644 --- a/Projects/Features/StorageFeature/Sources/ViewControllers/MyPlayListViewController.swift +++ b/Projects/Features/StorageFeature/Sources/ViewControllers/MyPlayListViewController.swift @@ -1,6 +1,7 @@ import BaseFeature import BaseFeatureInterface import DesignSystem +import LogManager import NVActivityIndicatorView import PanModal import PlaylistFeatureInterface @@ -8,14 +9,15 @@ import RxCocoa import RxDataSources import RxRelay import RxSwift +import SignInFeatureInterface import SongsDomainInterface import UIKit import UserDomainInterface import Utility -public typealias MyPlayListSectionModel = SectionModel +typealias MyPlayListSectionModel = SectionModel -public final class MyPlayListViewController: BaseViewController, ViewControllerFromStoryBoard, SongCartViewType { +final class MyPlayListViewController: BaseStoryboardReactorViewController, SongCartViewType { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var activityIndicator: NVActivityIndicatorView! @@ -23,193 +25,201 @@ public final class MyPlayListViewController: BaseViewController, ViewControllerF var multiPurposePopUpFactory: MultiPurposePopUpFactory! var textPopUpFactory: TextPopUpFactory! var playlistDetailFactory: PlaylistDetailFactory! - var viewModel: MyPlayListViewModel! - - lazy var input = MyPlayListViewModel.Input() - lazy var output = viewModel.transform(from: input) - var disposeBag = DisposeBag() + var signInFactory: SignInFactory! public var songCartView: SongCartView! public var bottomSheetView: BottomSheetView! let playState = PlayState.shared + let header = MyPlayListHeaderView(frame: CGRect(x: 0, y: 0, width: APP_WIDTH(), height: 140)) + override public func viewDidLoad() { super.viewDidLoad() - configureUI() - inputBindRx() - outputBindRx() } - public static func viewController( - viewModel: MyPlayListViewModel, + static func viewController( + reactor: MyPlaylistReactor, multiPurposePopUpFactory: MultiPurposePopUpFactory, playlistDetailFactory: PlaylistDetailFactory, - textPopUpFactory: TextPopUpFactory + textPopUpFactory: TextPopUpFactory, + signInFactory: SignInFactory ) -> MyPlayListViewController { let viewController = MyPlayListViewController.viewController(storyBoardName: "Storage", bundle: Bundle.module) - viewController.viewModel = viewModel + viewController.reactor = reactor viewController.multiPurposePopUpFactory = multiPurposePopUpFactory viewController.playlistDetailFactory = playlistDetailFactory viewController.textPopUpFactory = textPopUpFactory + viewController.signInFactory = signInFactory return viewController } -} -extension MyPlayListViewController { - private func inputBindRx() { + override func configureUI() { + super.configureUI() + + self.tableView.refreshControl = self.refreshControl + + header.delegate = self + self.tableView.tableHeaderView = header + + self.tableView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 56, right: 0) + + self.activityIndicator.type = .circleStrokeSpin + self.activityIndicator.color = DesignSystemAsset.PrimaryColor.point.color + self.activityIndicator.startAnimating() + + reactor?.action.onNext(.viewDidLoad) + } + + override func bind(reactor: MyPlaylistReactor) { + super.bind(reactor: reactor) + + tableView.rx.setDelegate(self) + .disposed(by: disposeBag) + } + + override func bindAction(reactor: MyPlaylistReactor) { + super.bindAction(reactor: reactor) + + let currentState = reactor.state + refreshControl.rx .controlEvent(.valueChanged) - .bind(to: input.playListLoad) + .map { Reactor.Action.refresh } + .bind(to: reactor.action) .disposed(by: disposeBag) tableView.rx.itemSelected - .withLatestFrom(output.dataSource) { ($0, $1) } - .withLatestFrom(output.state) { ($0.0, $0.1, $1) } - .subscribe(onNext: { [weak self] indexPath, dataSource, state in - guard let self = self else { return } - - let isEditing: Bool = state.isEditing - - if isEditing { // 편집 중일 때, 동작 안함 - self.input.itemSelected.onNext(indexPath) + .withUnretained(self) + .withLatestFrom(currentState.map(\.isEditing)) { ($0.0, $0.1, $1) } + .withLatestFrom(currentState.map(\.dataSource)) { ($0.0, $0.1, $0.2, $1) } + .bind { owner, indexPath, isEditing, dataSource in + + guard isEditing else { + owner.navigationController?.pushViewController( + owner.playlistDetailFactory.makeView( + id: dataSource[indexPath.section].items[indexPath.row].key, + isCustom: true + ), + animated: true + ) - } else { - let id: String = dataSource[indexPath.section].items[indexPath.row].key - let vc = self.playlistDetailFactory.makeView(id: id, isCustom: true) - self.navigationController?.pushViewController(vc, animated: true) + return } - }) + } .disposed(by: disposeBag) tableView.rx.itemMoved - .debug("itemMoved") - .bind(to: input.itemMoved) + .map { Reactor.Action.itemMoved($0) } + .bind(to: reactor.action) .disposed(by: disposeBag) } - private func outputBindRx() { - output.state - .skip(1) - .subscribe(onNext: { [weak self] state in - guard let self = self else { - return - } - if state.isEditing == false && state.force == false { // 정상적인 편집 완료 이벤트 - self.input.runEditing.onNext(()) - } + override func bindState(reactor: MyPlaylistReactor) { + super.bindState(reactor: reactor) - guard let parent = self.parent?.parent as? AfterLoginViewController else { - return - } + let sharedState = reactor.state.share(replay: 3) - // 탭맨 쪽 편집 변경 - let isEdit: Bool = state.isEditing - parent.output.state.accept(EditState(isEditing: isEdit, force: true)) - self.tableView.refreshControl = isEdit ? nil : self.refreshControl - self.tableView.setEditing(isEdit, animated: true) + sharedState.map(\.dataSource) + .skip(1) + .withUnretained(self) + .withLatestFrom(Utility.PreferenceManager.$userInfo) { ($0.0, $0.1, $1) } + .do(onNext: { owner, dataSource, userInfo in - let header = MyPlayListHeaderView(frame: CGRect(x: 0, y: 0, width: APP_WIDTH(), height: 140)) - header.delegate = self - self.tableView.tableHeaderView = isEdit ? nil : header - self.tableView.reloadData() - }) - .disposed(by: disposeBag) + owner.activityIndicator.stopAnimating() + owner.refreshControl.endRefreshing() - tableView.rx.setDelegate(self).disposed(by: disposeBag) + guard let userInfo = userInfo else { + // 로그인 안되어있음 - output.dataSource - .skip(1) - .do(onNext: { [weak self] model in - guard let self = self else { + let view = LoginWarningView( + frame: CGRect(x: .zero, y: .zero, width: APP_WIDTH(), height: APP_HEIGHT() / 5), + text: "로그인 하고\n리스트를 확인해보세요." + ) { + // TODO: 로그인 팝업 요청 (아마 StorageVC로 가야할 듯? + LogManager.printDebug("TAP 로그인 버튼") + } + + owner.tableView.tableFooterView = view return } - self.refreshControl.endRefreshing() - self.activityIndicator.stopAnimating() let warningView = WarningView(frame: CGRect(x: 0, y: 0, width: APP_WIDTH(), height: APP_HEIGHT() / 3)) warningView.text = "내 리스트가 없습니다." - let items = model.first?.items ?? [] - self.tableView.tableFooterView = items.isEmpty ? warningView : UIView(frame: CGRect( + let items = dataSource.first?.items ?? [] + owner.tableView.tableFooterView = items.isEmpty ? warningView : UIView(frame: CGRect( x: 0, y: 0, width: APP_WIDTH(), height: 56 )) }) + .map { $0.1 } .bind(to: tableView.rx.items(dataSource: createDatasources())) .disposed(by: disposeBag) - output.indexPathOfSelectedPlayLists - .skip(1) - .debug("indexPathOfSelectedPlayLists") - .withLatestFrom(output.dataSource) { ($0, $1) } - .subscribe(onNext: { [weak self] songs, dataSource in - guard let self = self else { return } - let items = dataSource.first?.items ?? [] + sharedState.map(\.isEditing) + .withUnretained(self) + .bind { owner, flag in - switch songs.isEmpty { - case true: - self.hideSongCart() - case false: - self.showSongCart( + owner.tableView.tableHeaderView = flag ? nil : owner.header + owner.tableView.isEditing = flag + owner.tableView.reloadData() + } + .disposed(by: disposeBag) + + sharedState.map(\.selectedItemCount) + .withUnretained(self) + .bind(onNext: { owner, count in + + if count == 0 { + owner.hideSongCart() + } else { + owner.showSongCart( in: UIApplication.shared.windows.first?.rootViewController?.view ?? UIView(), type: .myList, - selectedSongCount: songs.count, - totalSongCount: items.count, + selectedSongCount: count, + totalSongCount: owner.reactor?.currentState.dataSource.first?.items.count ?? 0, useBottomSpace: true ) - self.songCartView?.delegate = self + owner.songCartView?.delegate = owner } - }).disposed(by: disposeBag) - output.willAddPlayList - .skip(1) - .debug("willAddPlayList") - .subscribe(onNext: { [weak self] songs in - guard let self = self else { return } - if !songs.isEmpty { - self.playState.appendSongsToPlaylist(songs) - } - self.input.allPlayListSelected.onNext(false) - self.output.state.accept(EditState(isEditing: false, force: true)) - let message: String = songs.isEmpty ? - "리스트에 곡이 없습니다." : - "\(songs.count)곡이 재생목록에 추가되었습니다. 중복 곡은 제외됩니다." - self.showToast( - text: message, - font: DesignSystemFontFamily.Pretendard.light.font(size: 14) - ) - }).disposed(by: disposeBag) - - output.showToast - .subscribe(onNext: { [weak self] result in - guard let self = self else { - return - } - - self.showToast( - text: result.description, - font: DesignSystemFontFamily.Pretendard.light.font(size: 14) - ) }) .disposed(by: disposeBag) - - output.onLogout.bind(with: self) { owner, error in - NotificationCenter.default.post(name: .movedTab, object: 4) - owner.showToast( - text: error.localizedDescription, - font: DesignSystemFontFamily.Pretendard.light.font(size: 14) - ) - } - .disposed(by: disposeBag) } + // extension MyPlayListViewController { + +// +// output.showToast +// .subscribe(onNext: { [weak self] result in +// guard let self = self else { +// return +// } +// +// self.showToast( +// text: result.description, +// font: DesignSystemFontFamily.Pretendard.light.font(size: 14) +// ) +// }) +// .disposed(by: disposeBag) +// +// output.onLogout.bind(with: self) { owner, error in +// NotificationCenter.default.post(name: .movedTab, object: 4) +// owner.showToast( +// text: error.localizedDescription, +// font: DesignSystemFontFamily.Pretendard.light.font(size: 14) +// ) +// } +// .disposed(by: disposeBag) +// } + private func createDatasources() -> RxTableViewSectionedReloadDataSource { let datasource = RxTableViewSectionedReloadDataSource( configureCell: { [weak self] _, tableView, indexPath, model -> UITableViewCell in - guard let self = self else { return UITableViewCell() } + guard let self = self, let reactor = self.reactor else { return UITableViewCell() } guard let cell = tableView.dequeueReusableCell( withIdentifier: "MyPlayListTableViewCell", @@ -219,7 +229,7 @@ extension MyPlayListViewController { cell.update( model: model, - isEditing: self.output.state.value.isEditing, + isEditing: reactor.currentState.isEditing, indexPath: indexPath ) cell.delegate = self @@ -236,50 +246,40 @@ extension MyPlayListViewController { ) return datasource } - - private func configureUI() { - self.tableView.refreshControl = self.refreshControl - let header = MyPlayListHeaderView(frame: CGRect(x: 0, y: 0, width: APP_WIDTH(), height: 140)) - header.delegate = self - self.tableView.tableHeaderView = header - self.tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: APP_WIDTH(), height: 56)) - self.tableView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 56, right: 0) - self.activityIndicator.type = .circleStrokeSpin - self.activityIndicator.color = DesignSystemAsset.PrimaryColor.point.color - self.activityIndicator.startAnimating() - } } extension MyPlayListViewController: SongCartViewDelegate { public func buttonTapped(type: SongCartSelectType) { switch type { case let .allSelect(flag): - input.allPlayListSelected.onNext(flag) + reactor?.action.onNext(.tapAll(isSelecting: flag)) case .addPlayList: - input.addPlayList.onNext(()) + // input.addPlayList.onNext(()) self.hideSongCart() case .remove: - let count: Int = output.indexPathOfSelectedPlayLists.value.count - - guard let textPopupViewController = self.textPopUpFactory.makeView( - text: "선택한 내 리스트 \(count)개가 삭제됩니다.", - cancelButtonIsHidden: false, - allowsDragAndTapToDismiss: nil, - confirmButtonText: nil, - cancelButtonText: nil, - completion: { [weak self] in - - guard let self else { return } - self.input.deletePlayList.onNext(()) - self.hideSongCart() - - }, - cancelCompletion: nil - ) as? TextPopupViewController else { - return - } + // TODO: useCase 연결 후 + break +// let count: Int = output.indexPathOfSelectedPlayLists.value.count +// +// guard let textPopupViewController = self.textPopUpFactory.makeView( +// text: "선택한 내 리스트 \(count)개가 삭제됩니다.", +// cancelButtonIsHidden: false, +// allowsDragAndTapToDismiss: nil, +// confirmButtonText: nil, +// cancelButtonText: nil, +// completion: { [weak self] in +// +// guard let self else { return } +// self.input.deletePlayList.onNext(()) +// self.hideSongCart() +// +// }, +// cancelCompletion: nil +// ) as? TextPopupViewController else { +// return +// } default: return } @@ -290,17 +290,19 @@ extension MyPlayListViewController: MyPlayListTableViewCellDelegate { public func buttonTapped(type: MyPlayListTableViewCellDelegateConstant) { switch type { case let .listTapped(indexPath): - input.itemSelected.onNext(indexPath) + self.reactor?.action.onNext(.tapDidPlaylist(indexPath.row)) case let .playTapped(indexPath): - let songs: [SongEntity] = output.dataSource.value[indexPath.section].items[indexPath.row].songlist - guard !songs.isEmpty else { - self.showToast( - text: "리스트에 곡이 없습니다.", - font: DesignSystemFontFamily.Pretendard.light.font(size: 14) - ) - return - } - self.playState.loadAndAppendSongsToPlaylist(songs) + // TODO: useCase 연결 후 + break +// let songs: [SongEntity] = output.dataSource.value[indexPath.section].items[indexPath.row].songlist +// guard !songs.isEmpty else { +// self.showToast( +// text: "리스트에 곡이 없습니다.", +// font: DesignSystemFontFamily.Pretendard.light.font(size: 14) +// ) +// return +// } +// self.playState.loadAndAppendSongsToPlaylist(songs) } } } @@ -322,20 +324,44 @@ extension MyPlayListViewController: UITableViewDelegate { extension MyPlayListViewController: MyPlayListHeaderViewDelegate { public func action(_ type: PurposeType) { - if let parent = self.parent?.parent as? AfterLoginViewController { - parent.hideEditSheet() - parent.profileButton.isSelected = false + guard let userInfo = Utility.PreferenceManager.userInfo else { + guard let vc = self.textPopUpFactory.makeView( + text: "로그인이 필요한 서비스입니다.\n로그인 하시겠습니까?", + cancelButtonIsHidden: false, + allowsDragAndTapToDismiss: nil, + confirmButtonText: nil, + cancelButtonText: nil, + completion: { [weak self] in + + guard let self else { return } + + let loginVC = self.signInFactory.makeView() + self.present(loginVC, animated: true) + }, + cancelCompletion: {} + ) as? TextPopupViewController else { + return + } + + self.showPanModal(content: vc) + + return + } + + switch type { + // TODO: UseCase 연결 후 + case .creation: + break + case .share: + break + default: + break } - let vc = multiPurposePopUpFactory - .makeView(type: type, key: "", completion: nil) - self.showEntryKitModal(content: vc, height: 296) } } extension MyPlayListViewController { func scrollToTop() { - let itemIsEmpty: Bool = output.dataSource.value.first?.items.isEmpty ?? true - guard !itemIsEmpty else { return } tableView.setContentOffset(.zero, animated: true) } } diff --git a/Projects/Features/StorageFeature/Sources/ViewControllers/StorageViewController.swift b/Projects/Features/StorageFeature/Sources/ViewControllers/StorageViewController.swift index 0f8566bfd..66b856557 100644 --- a/Projects/Features/StorageFeature/Sources/ViewControllers/StorageViewController.swift +++ b/Projects/Features/StorageFeature/Sources/ViewControllers/StorageViewController.swift @@ -1,84 +1,287 @@ import BaseFeature +import BaseFeatureInterface import DesignSystem import KeychainModule -import RxCocoa -import RxRelay +import Pageboy +import PanModal +import ReactorKit import RxSwift import SignInFeatureInterface +import Tabman import UIKit import Utility -public final class StorageViewController: BaseViewController, ViewControllerFromStoryBoard, ContainerViewType, - EqualHandleTappedType { - @IBOutlet public weak var contentView: UIView! +final class StorageViewController: TabmanViewController, ViewControllerFromStoryBoard, EqualHandleTappedType, + StoryboardView { + private enum Color { + static let gray = DesignSystemAsset.GrayColor.gray400.color - var signInFactory: SignInFactory! - var afterLoginComponent: AfterLoginComponent! + static let point = DesignSystemAsset.PrimaryColor.point.color + } + + private enum ButtonAttributed { + static let edit = NSMutableAttributedString( + string: "편집", + attributes: [ + .font: DesignSystemFontFamily.Pretendard.bold.font(size: 12), + .foregroundColor: Color.gray + ] + ) + + static let save = NSMutableAttributedString( + string: "완료", + attributes: [ + .font: DesignSystemFontFamily.Pretendard.bold.font(size: 12), + .foregroundColor: Color.point + ] + ) + } + + typealias Reactor = StorageReactor + + var disposeBag: DisposeBag = DisposeBag() + + @IBOutlet weak var tabBarView: UIView! + @IBOutlet weak var editButton: UIButton! + @IBOutlet weak var saveButton: UIButton! + @IBOutlet weak var myPlayListFakeView: UIView! + @IBOutlet weak var favoriteFakeView: UIView! + + public var bottomSheetView: BottomSheetView! + private var myPlayListComponent: MyPlayListComponent! + private var multiPurposePopUpFactory: MultiPurposePopUpFactory! + private var favoriteComponent: FavoriteComponent! + private var textPopUpFactory: TextPopUpFactory! - lazy var bfLoginView = signInFactory.makeView() - lazy var afLoginView = afterLoginComponent.makeView() - let disposeBag = DisposeBag() + private lazy var viewControllers: [UIViewController] = [ + myPlayListComponent.makeView(), + favoriteComponent.makeView() + ] + + private var signInFactory: SignInFactory! override public func viewDidLoad() { super.viewDidLoad() configureUI() } + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + + /// 탭맨 페이지 변경 감지 함수 + override public func pageboyViewController( + _ pageboyViewController: PageboyViewController, + didScrollToPageAt index: TabmanViewController.PageIndex, + direction: PageboyViewController.NavigationDirection, + animated: Bool + ) { + // TODO: 편집 모드 처리 + } + public static func viewController( - signInFactory: SignInFactory, - afterLoginComponent: AfterLoginComponent + reactor: StorageReactor, + myPlayListComponent: MyPlayListComponent, + multiPurposePopUpFactory: MultiPurposePopUpFactory, + favoriteComponent: FavoriteComponent, + textPopUpFactory: TextPopUpFactory, + signInFactory: SignInFactory + ) -> StorageViewController { let viewController = StorageViewController.viewController(storyBoardName: "Storage", bundle: Bundle.module) + + viewController.reactor = reactor + + viewController.myPlayListComponent = myPlayListComponent + viewController.multiPurposePopUpFactory = multiPurposePopUpFactory + viewController.favoriteComponent = favoriteComponent + viewController.viewControllers = [myPlayListComponent.makeView(), favoriteComponent.makeView()] + viewController.textPopUpFactory = textPopUpFactory viewController.signInFactory = signInFactory - viewController.afterLoginComponent = afterLoginComponent return viewController } + + deinit { + DEBUG_LOG("❌ \(Self.self)") + } + + func bind(reactor: StorageReactor) { + bindAction(reactor: reactor) + bindState(reactor: reactor) + } +} + +extension StorageViewController { + func bindAction(reactor: Reactor) { + editButton.rx + .tap + .withUnretained(self) + .withLatestFrom(Utility.PreferenceManager.$userInfo) { ($0.0, $1) } + .bind { owner, userInfo in + + // TODO: 나중에(USECASE 연결 후) 주석 해제 +// guard let userInfo = userInfo else { +// reactor.action.onNext(.showLoginAlert) // 로그인 화면 팝업 +// return +// } + + reactor.action.onNext(.tabDidEditButton) + } + .disposed(by: disposeBag) + + saveButton.rx + .tap + .map { Reactor.Action.saveButtonTap } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + func bindState(reactor: Reactor) { + let sharedState = reactor.state.share(replay: 1) + + sharedState.map(\.isEditing) + .withUnretained(self) + .bind { owner, flag in + + owner.isScrollEnabled = !flag // 편집 시 , 옆 탭으로 swipe를 막기 위함 + owner.editButton.isHidden = flag + owner.saveButton.isHidden = !flag + + if flag { + owner.myPlayListFakeView.isHidden = owner.currentIndex == 0 + owner.favoriteFakeView.isHidden = owner.currentIndex == 1 + + } else { + owner.myPlayListFakeView.isHidden = true + owner.favoriteFakeView.isHidden = true + } + } + .disposed(by: disposeBag) + + reactor.pulse(\.$showLoginAlert) + .skip(1) + .withUnretained(self) + .bind { owner, _ in + + guard let vc = owner.textPopUpFactory.makeView( + text: "로그인이 필요한 서비스입니다.\n로그인 하시겠습니까?", + cancelButtonIsHidden: false, + allowsDragAndTapToDismiss: nil, + confirmButtonText: nil, + cancelButtonText: nil, + completion: { + let loginVC = owner.signInFactory.makeView() + owner.present(loginVC, animated: true) + }, + cancelCompletion: {} + ) as? TextPopupViewController else { + return + } + + owner.showPanModal(content: vc) + } + .disposed(by: disposeBag) + } } extension StorageViewController { private func configureUI() { - bindRx() + editButton.layer.cornerRadius = 4 + editButton.layer.borderWidth = 1 + editButton.backgroundColor = .clear + + editButton.layer.borderColor = DesignSystemAsset.GrayColor.gray300.color.cgColor + + editButton.setAttributedTitle(ButtonAttributed.edit, for: .normal) + + saveButton.layer.cornerRadius = 4 + saveButton.layer.borderWidth = 1 + saveButton.backgroundColor = .clear + saveButton.layer.borderColor = Color.point.cgColor + saveButton.setAttributedTitle(ButtonAttributed.save, for: .normal) + saveButton.isHidden = true + + myPlayListFakeView.isHidden = true + favoriteFakeView.isHidden = true + + // 탭바 설정 + self.dataSource = self + let bar = TMBar.ButtonBar() + + // 배경색 + bar.backgroundView.style = .flat(color: .clear) + + // 간격 설정 + bar.layout.contentInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) + bar.layout.contentMode = .intrinsic + bar.layout.transitionStyle = .progressive + + // 버튼 글씨 커스텀 + bar.buttons.customize { button in + button.tintColor = DesignSystemAsset.GrayColor.gray400.color + button.selectedTintColor = DesignSystemAsset.GrayColor.gray900.color + button.font = DesignSystemFontFamily.Pretendard.medium.font(size: 16) + button.selectedFont = DesignSystemFontFamily.Pretendard.bold.font(size: 16) + } + + // indicator + bar.indicator.weight = .custom(value: 3) + bar.indicator.tintColor = DesignSystemAsset.PrimaryColor.point.color + bar.indicator.overscrollBehavior = .compress + addBar(bar, dataSource: self, at: .custom(view: tabBarView, layout: nil)) + bar.layer.addBorder( + [.bottom], + color: DesignSystemAsset.GrayColor.gray300.color.withAlphaComponent(0.4), + height: 1 + ) } +} - private func bindRx() { - // TODO: 나중에 보관함 작업 리팩할 때 +extension StorageViewController: PageboyViewControllerDataSource, TMBarDataSource { + public func numberOfViewControllers(in pageboyViewController: Pageboy.PageboyViewController) -> Int { + self.viewControllers.count + } -// Utility.PreferenceManager.$userInfo -// .map { $0 != nil } -// .subscribe(onNext: { [weak self] isLogin in -// guard let self = self else { -// return -// } -// DEBUG_LOG(isLogin) -// -// if isLogin { -// if let _ = self.children.first as? LoginViewController { -// self.remove(asChildViewController: self.bfLoginView) -// } -// self.add(asChildViewController: self.afLoginView) -// -// } else { -// if let _ = self.children.first as? AfterLoginViewController { -// self.remove(asChildViewController: self.afLoginView) -// } -// self.add(asChildViewController: self.bfLoginView) -// } -// }).disposed(by: disposeBag) + public func viewController( + for pageboyViewController: Pageboy.PageboyViewController, + at index: Pageboy.PageboyViewController.PageIndex + ) -> UIViewController? { + viewControllers[index] + } + + public func defaultPage(for pageboyViewController: Pageboy.PageboyViewController) -> Pageboy.PageboyViewController + .Page? { + nil + } + + public func barItem(for bar: Tabman.TMBar, at index: Int) -> Tabman.TMBarItemable { + switch index { + case 0: + return TMBarItem(title: "내 리스트") + case 1: + return TMBarItem(title: "좋아요") + default: + let title = "Page \(index)" + return TMBarItem(title: title) + } } } -public extension StorageViewController { - // TODO: 나중에 보관함 작업 리팩할 때 - func equalHandleTapped() { -// let viewControllersCount: Int = self.navigationController?.viewControllers.count ?? 0 -// if viewControllersCount > 1 { -// self.navigationController?.popToRootViewController(animated: true) -// } else { -// if let nonLogin = children.first as? LoginViewController { -// nonLogin.scrollToTop() -// } else if let isLogin = children.first as? AfterLoginViewController { -// isLogin.scrollToTop() -// } -// } +extension StorageViewController { + func scrollToTop() { + let current: Int = self.currentIndex ?? 0 + guard self.viewControllers.count > current else { return } + if let myPlayList = self.viewControllers[current] as? MyPlayListViewController { + myPlayList.scrollToTop() + } else if let favorite = self.viewControllers[current] as? FavoriteViewController { + favorite.scrollToTop() + } + } + + public func equalHandleTapped() { + let viewControllersCount: Int = self.navigationController?.viewControllers.count ?? 0 + if viewControllersCount > 1 { + self.navigationController?.popToRootViewController(animated: true) + } } } diff --git a/Projects/Features/StorageFeature/interface/StorageFactory.swift b/Projects/Features/StorageFeature/interface/StorageFactory.swift new file mode 100644 index 000000000..f478f0d2f --- /dev/null +++ b/Projects/Features/StorageFeature/interface/StorageFactory.swift @@ -0,0 +1,5 @@ +import UIKit + +public protocol StorageFactory { + func makeView() -> UIViewController +} diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/Views/LoginWarningView.swift b/Projects/UsertInterfaces/DesignSystem/Sources/Views/LoginWarningView.swift new file mode 100644 index 000000000..8c0ae0d7e --- /dev/null +++ b/Projects/UsertInterfaces/DesignSystem/Sources/Views/LoginWarningView.swift @@ -0,0 +1,87 @@ +import SnapKit +import Then +import UIKit + +public final class LoginWarningView: UIView { + private let completion: () -> Void + + private let imageView: UIImageView = UIImageView().then { + $0.contentMode = .scaleAspectFill + $0.image = DesignSystemAsset.Search.warning.image + } + + private let label: UILabel = UILabel().then { + $0.font = DesignSystemFontFamily.Pretendard.medium.font(size: 14) + $0.textColor = DesignSystemAsset.BlueGrayColor.blueGray900.color + $0.textAlignment = .center + $0.backgroundColor = .clear + $0.numberOfLines = .zero + } + + private let button: UIButton = UIButton().then { + $0.titleLabel?.font = DesignSystemFontFamily.Pretendard.medium.font(size: 14) + $0.setTitle("로그인", for: .normal) + $0.setTitleColor(DesignSystemAsset.BlueGrayColor.blueGray600.color, for: .normal) + $0.layer.cornerRadius = 8 + $0.layer.borderColor = DesignSystemAsset.BlueGrayColor.blueGray400.color.cgColor + $0.layer.borderWidth = 1 + $0.clipsToBounds = true + } + + public init( + frame: CGRect = CGRect( + x: .zero, + + y: .zero, + width: 164, + height: 176 + ), + text: String = "로그인을 해주세요.", + _ completion: @escaping (() -> Void) + ) { + self.completion = completion + super.init(frame: frame) + + self.addSubview(imageView) + self.addSubview(label) + self.addSubview(button) + + label.text = text + + configureUI() + + button.addTarget(self, action: #selector(tapLoginButton), for: .touchUpInside) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension LoginWarningView { + private func configureUI() { + imageView.snp.makeConstraints { + $0.width.height.equalTo(80) + $0.top.equalToSuperview() + $0.centerX.equalToSuperview() + } + + label.snp.makeConstraints { + $0.top.equalTo(imageView.snp.bottom).offset(-8) + $0.leading.trailing.equalToSuperview() + $0.centerX.equalTo(imageView.snp.centerX) + } + + button.snp.makeConstraints { + $0.height.equalTo(44) + $0.width.equalTo(164) + $0.top.equalTo(label.snp.bottom).offset(16) + $0.centerX.equalToSuperview() + } + } + + @objc func tapLoginButton() { + completion() + } +}