diff --git a/.gitignore b/.gitignore index ed421b74..7cf52ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ Moneymong.app.dSYM.zip fastlane/report.xml fastlane/README.md Projects/App/Resources/APIKey.xcconfig +graph.png diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index ae08c7cb..db22b740 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -8,7 +8,7 @@ let project = Project( ), settings: .settings( base: .init() - .marketingVersion("1.3.2") + .marketingVersion("1.4.0") .swiftVersion("5.7") .currentProjectVersion("1") .appleGenericVersioningSystem(), @@ -99,6 +99,7 @@ let project = Project( dependencies: [ .project(target: "SignFeature", path: .relativeToRoot("Projects/Feature/Sign")), .project(target: "MainFeature", path: .relativeToRoot("Projects/Feature/Main")), + .project(target: "CreateAgency", path: .relativeToRoot("Projects/Feature/CreateAgency")), .target(name: "WidgetExtension") ], settings: .settings( diff --git a/Projects/App/Sources/AppCoordinator.swift b/Projects/App/Sources/AppCoordinator.swift index 2ec6c28b..f7a0a671 100644 --- a/Projects/App/Sources/AppCoordinator.swift +++ b/Projects/App/Sources/AppCoordinator.swift @@ -1,6 +1,6 @@ import UIKit -import BaseFeature +import BaseFeatureInterface import MainFeature import SignFeature import DesignSystem @@ -26,6 +26,14 @@ final class AppCoordinator: Coordinator { main(animated: true) case .login: sign(animated: true) + case .ledger: + main(animated: true) + let mainCoordinator = childCoordinators.first { $0 is MainTabBarCoordinator } + mainCoordinator?.move(to: .ledger) + case let .createManualLedger(id): + main(animated: true) + let mainCoordinator = childCoordinators.first { $0 is MainTabBarCoordinator } + mainCoordinator?.move(to: .createManualLedger(id)) default: break } } diff --git a/Projects/App/Sources/DIContainer.swift b/Projects/App/Sources/DIContainer.swift index d48ad847..3229bb37 100644 --- a/Projects/App/Sources/DIContainer.swift +++ b/Projects/App/Sources/DIContainer.swift @@ -3,6 +3,7 @@ import SignFeature import AgencyFeature import LedgerFeature import MyPageFeature +import CreateAgency import Core @@ -25,12 +26,14 @@ final class AppDIContainer { self.signDIContainer = SignDIContainer( localStorage: localStorage, - networkManager: networkManager + networkManager: networkManager, + inputAgencyInfoFactory: InputAgencyInfoFactory(networkManager: networkManager, localStorage: localStorage) ) self.mainDIContainer = MainDIContainer( localStorage: localStorage, - networkManager: networkManager + networkManager: networkManager, + inputAgencyInfoFactory: InputAgencyInfoFactory(networkManager: networkManager, localStorage: localStorage) ) } } diff --git a/Projects/Core/Core/Sources/Network/Common/NetworkManager.swift b/Projects/Core/Core/Sources/Network/Common/NetworkManager.swift index 78b94f12..1f895972 100644 --- a/Projects/Core/Core/Sources/Network/Common/NetworkManager.swift +++ b/Projects/Core/Core/Sources/Network/Common/NetworkManager.swift @@ -110,7 +110,6 @@ public final class NetworkManager: NetworkManagerInterfacae { throw MoneyMongError.appError(.default, errorMessage: "디코딩 실패") case let .failure(error): - assertionFailure("서버동작 에러! 적절한 처리 필요 \(error.localizedDescription)") throw error } } diff --git a/Projects/Feature/Agency/Project.swift b/Projects/Feature/Agency/Project.swift index ded5787e..1a03425e 100644 --- a/Projects/Feature/Agency/Project.swift +++ b/Projects/Feature/Agency/Project.swift @@ -15,7 +15,8 @@ let project = Project( deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), sources: ["Sources/**"], dependencies: [ - .project(target: "BaseFeature", path: .relativeToRoot("Projects/Feature/Base")) + .project(target: "BaseFeature", path: .relativeToRoot("Projects/Feature/Base")), + .project(target: "CreateAgencyInterface", path: .relativeToRoot("Projects/Feature/CreateAgency")) ], settings: .settings(base: [ "SWIFT_VERSION": "5.7" diff --git a/Projects/Feature/Agency/Sources/AgencyCoordinator.swift b/Projects/Feature/Agency/Sources/AgencyCoordinator.swift index 90b19ba7..47788e61 100644 --- a/Projects/Feature/Agency/Sources/AgencyCoordinator.swift +++ b/Projects/Feature/Agency/Sources/AgencyCoordinator.swift @@ -1,7 +1,8 @@ import UIKit -import BaseFeature +import BaseFeatureInterface import DesignSystem +import CreateAgencyInterface public final class AgencyCoordinator: Coordinator { public var navigationController: UINavigationController @@ -10,7 +11,7 @@ public final class AgencyCoordinator: Coordinator { public var childCoordinators: [Coordinator] = [] weak var secondFlowNavigationController: UINavigationController? - + public init(navigationController: UINavigationController, diContainer: AgencyDIContainer) { self.navigationController = navigationController self.diContainer = diContainer @@ -20,8 +21,7 @@ public final class AgencyCoordinator: Coordinator { case alert(title: String, subTitle: String?, okAction: () -> Void, cancelAction: (() -> Void)? = nil) case joinAgency(id: Int, name: String) case joinComplete - case createAgency - case createComplete(id: Int) + case createAgency(UniversityType) case web(String) } @@ -29,6 +29,16 @@ public final class AgencyCoordinator: Coordinator { agency(animated: animated) } + public func move(to scene: BaseFeatureInterface.Scene) { + switch scene { + case .ledger: + parentCoordinator?.move(to: .ledger) + case .createManualLedger(let int): + parentCoordinator?.move(to: .createManualLedger(int)) + default: break + } + } + func present(_ scene: Scene, animated: Bool = true) { switch scene { case let .alert(title, subTitle, okAction, cancelAction): @@ -41,10 +51,8 @@ public final class AgencyCoordinator: Coordinator { joinAgency(id: id, name: name, animated: animated) case .joinComplete: joinComplete(animated: animated) - case .createAgency: - createAgency(animated: animated) - case let .createComplete(id): - createComplete(agencyID: id, animated: animated) + case let .createAgency(universityType): + createAgency(universityType: universityType, animated: animated) case let .web(url): web(urlString: url) } @@ -54,13 +62,9 @@ public final class AgencyCoordinator: Coordinator { navigationController.topViewController?.dismiss(animated: animated) } - func goLedger() { + public func goLedger() { parentCoordinator?.move(to: .ledger) } - - func goManualInput(agencyID: Int) { - parentCoordinator?.move(to: .createManualLedger(agencyID)) - } } extension AgencyCoordinator { @@ -69,16 +73,10 @@ extension AgencyCoordinator { navigationController.viewControllers = [vc] } - private func createAgency(animated: Bool) { - let vc = diContainer.createAgency(with: self) - secondFlowNavigationController = vc as? UINavigationController + private func createAgency(universityType: UniversityType, animated: Bool) { + let vc = diContainer.createAgency(with: self, universityType: universityType) vc.modalPresentationStyle = .fullScreen - navigationController.topViewController?.present(vc, animated: animated) - } - - private func createComplete(agencyID: Int, animated: Bool) { - let vc = diContainer.createComplete(with: self, id: agencyID) - secondFlowNavigationController?.pushViewController(vc, animated: animated) + navigationController.present(vc, animated: animated) } private func joinAgency(id: Int, name: String, animated: Bool) { diff --git a/Projects/Feature/Agency/Sources/AgencyDIContainer.swift b/Projects/Feature/Agency/Sources/AgencyDIContainer.swift index 0329f9d4..18176576 100644 --- a/Projects/Feature/Agency/Sources/AgencyDIContainer.swift +++ b/Projects/Feature/Agency/Sources/AgencyDIContainer.swift @@ -1,6 +1,7 @@ import UIKit import Core +import CreateAgencyInterface public final class AgencyDIContainer { @@ -10,36 +11,33 @@ public final class AgencyDIContainer { private let agencyRepo: AgencyRepositoryInterface private let userRepo: UserRepositoryInterface + private let inputAgencyInfoFactory: InputAgencyInfoFactoryInterface + public init( localStorage: LocalStorageInterface, - networkManager: NetworkManagerInterfacae + networkManager: NetworkManagerInterfacae, + inputAgencyInfoFactory: InputAgencyInfoFactoryInterface ) { self.localStorage = localStorage self.networkManager = networkManager self.agencyRepo = AgencyRepository(networkManager: networkManager) self.userRepo = UserRepository(networkManager: networkManager, localStorage: localStorage) + self.inputAgencyInfoFactory = inputAgencyInfoFactory } func agency(with coordinator: AgencyCoordinator) -> AgencyListVC { let vc = AgencyListVC() - vc.reactor = AgencyListReactor(agencyRepo: agencyRepo) + vc.reactor = AgencyListReactor(agencyRepo: agencyRepo, userRepo: userRepo) vc.coordinator = coordinator return vc } - func createAgency(with coordinator: AgencyCoordinator) -> UIViewController { - let vc = CreateAgencyVC() - let rootVC = UINavigationController(rootViewController: vc) - vc.reactor = CreateAgencyReactor(agencyRepo: agencyRepo, userRepo: userRepo) - vc.coordinator = coordinator - return rootVC - } - - func createComplete(with coordinator: AgencyCoordinator, id: Int) -> CreateCompleteVC { - let vc = CreateCompleteVC() - vc.reactor = CreateCompleteReactor(userRepo: userRepo, id: id) - vc.coordinator = coordinator - return vc + func createAgency(with coordinator: AgencyCoordinator, universityType: UniversityType) -> UIViewController { + let navigationController = UINavigationController() + let createAgencyCoordinator = CreateAgencyCoordinator(navigationController: navigationController, inputAgencyFactory: inputAgencyInfoFactory) + createAgencyCoordinator.parentCoordinator = coordinator + createAgencyCoordinator.start(animated: true, universityType: universityType) + return navigationController } func joinAgency(id: Int, name: String, with coordinator: AgencyCoordinator) -> UIViewController { diff --git a/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListReactor.swift b/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListReactor.swift index d332d5c7..a1bff5d2 100644 --- a/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListReactor.swift +++ b/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListReactor.swift @@ -22,6 +22,7 @@ public final class AgencyListReactor: Reactor { case tapSearchButton // 키보드의 검색 버튼을 눌렀을떄 case tapCancelButton case searchTextChanged(String?) + case viewDidLoad } public enum Mutation { @@ -32,6 +33,7 @@ public final class AgencyListReactor: Reactor { case setAlert(title: String, subTitle: String) case setPage(Int) case setQuery(String?) + case setUserInfo(Result) } public struct State { @@ -40,6 +42,7 @@ public final class AgencyListReactor: Reactor { var page: Int = 0 @Pulse var myAgency: [Agency] = [] @Pulse var items: [Item] = [.feedback] + @Pulse var userInfo: UserInfo? @Pulse var error: MoneyMongError? @Pulse var isLoading = false @@ -53,11 +56,18 @@ public final class AgencyListReactor: Reactor { } public let initialState: State = State() + private let agencyRepo: AgencyRepositoryInterface + private let userRepo: UserRepositoryInterface + private let listLimit = 20 - init(agencyRepo: AgencyRepositoryInterface) { + init( + agencyRepo: AgencyRepositoryInterface, + userRepo: UserRepositoryInterface + ) { self.agencyRepo = agencyRepo + self.userRepo = userRepo } public func mutate(action: Action) -> Observable { @@ -132,6 +142,12 @@ public final class AgencyListReactor: Reactor { case let .searchTextChanged(query): return .just(.setQuery(query)) + case .viewDidLoad: + return .task { + try await userRepo.user() + } + .map { .setUserInfo(.success($0)) } + .catch { return .just(.setUserInfo(.failure($0.toMMError))) } } } @@ -168,6 +184,12 @@ public final class AgencyListReactor: Reactor { case let .setQuery(query): newState.query = query + + case let .setUserInfo(.success(userInfo)): + newState.userInfo = userInfo + + case let .setUserInfo(.failure(error)): + newState.error = error } return newState diff --git a/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListVC.swift b/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListVC.swift index 3cbebc94..351b278a 100644 --- a/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListVC.swift +++ b/Projects/Feature/Agency/Sources/Scene/AgencyList/AgencyListVC.swift @@ -4,6 +4,7 @@ import DesignSystem import BaseFeature import Utility import Core +import CreateAgencyInterface import ReactorKit import RxDataSources @@ -66,6 +67,10 @@ public final class AgencyListVC: BaseVC, View { public func bind(reactor: AgencyListReactor) { setRightItem(.search) // Action Binding + rx.viewDidLoad + .map { Reactor.Action.viewDidLoad } + .bind(to: reactor.action) + .disposed(by: disposeBag) navigationItem.rightBarButtonItem?.rx.tap .observe(on: MainScheduler.instance) @@ -123,8 +128,10 @@ public final class AgencyListVC: BaseVC, View { createAgencyButton.rx.tap .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) - .bind(with: self) { owner, _ in - owner.coordinator?.present(.createAgency) + .compactMap { reactor.currentState.userInfo?.universityName } + .map { universityName -> UniversityType in universityName == "정보없음" ? .none : .exists } + .bind(with: self) { owner, universityType in + owner.coordinator?.present(.createAgency(universityType)) } .disposed(by: disposeBag) @@ -142,7 +149,6 @@ public final class AgencyListVC: BaseVC, View { reactor.pulse(\.$query) .observe(on: MainScheduler.instance) .bind(with: self) { owner, query in - owner.navigationItem.rightBarButtonItem?.setValue(query != nil, forKey: "hidden") owner.searchHeaderView.flex.height(query == nil ? 0 : 60) owner.searchHeaderView.flex.markDirty() diff --git a/Projects/Feature/Agency/Sources/Scene/CreateAgency/CreateAgencyReactor.swift b/Projects/Feature/Agency/Sources/Scene/CreateAgency/CreateAgencyReactor.swift deleted file mode 100644 index ea910bb7..00000000 --- a/Projects/Feature/Agency/Sources/Scene/CreateAgency/CreateAgencyReactor.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Core - -import ReactorKit - -final class CreateAgencyReactor: Reactor { - - struct State { - @Pulse var userInfo: UserInfo? - @Pulse var index = 0 // 소속 종류: 동아리 or 학생회 - @Pulse var text = "" // 소속 이름 - @Pulse var isButtonEnabled = false - - @Pulse var isLoading = false - @Pulse var error: MoneyMongError? - - @Pulse var destination: Destination? - - enum Destination { - case complete(Int) - } - } - - enum Action { - case onAppear - case textFieldDidChange(String) - case selectedIndexDidChange(Int) - case tapCreateButton - } - - enum Mutation { - case setText(String) - case setError(MoneyMongError) - case setLoading(Bool) - case setSelectedIndex(Int) - case setButtonEnabled(Bool) - case setDestination(State.Destination) - case setUserInfo(UserInfo) - } - - public let initialState: State = State() - private let agencyRepo: AgencyRepositoryInterface - private let userRepo: UserRepositoryInterface - - init( - agencyRepo: AgencyRepositoryInterface, - userRepo: UserRepositoryInterface - ) { - self.agencyRepo = agencyRepo - self.userRepo = userRepo - } - - func mutate(action: Action) -> Observable { - switch action { - case .onAppear: - return .task { - try await userRepo.user() - } - .map { .setUserInfo($0) } - .catch { return .just(.setError($0.toMMError)) } - - case let .textFieldDidChange(text): - return .concat( - .just(.setText(text)), - .just(.setButtonEnabled((1...20) ~= text.count)) - ) - - case let .selectedIndexDidChange(index): - return .just(.setSelectedIndex(index)) - - case .tapCreateButton: - return .concat( - .just(.setLoading(true)), - .task { - let type: String - - switch currentState.index { - case 0: - type = "IN_SCHOOL_CLUB" - case 1: - type = "STUDENT_COUNCIL" - case 2: - type = "GENERAL" - default: - fatalError("Invalid type") - } - - return try await agencyRepo.create( - name: currentState.text, - type: type - ) - } - .map { .setDestination(.complete($0)) } - .catch { return .just(.setError($0.toMMError)) }, - - .just(.setLoading(false)) - ) - } - } - - func reduce(state: State, mutation: Mutation) -> State { - var newState = state - - switch mutation { - case let .setText(text): - newState.text = text - case let .setButtonEnabled(value): - newState.isButtonEnabled = value - case let .setSelectedIndex(index): - newState.index = index - case let .setDestination(value): - newState.destination = value - case let .setError(value): - newState.error = value - case let .setLoading(value): - newState.isLoading = value - case let .setUserInfo(value): - newState.userInfo = value - } - - return newState - } -} diff --git a/Projects/Feature/Agency/Sources/Scene/CreateAgency/CreateAgencyVC.swift b/Projects/Feature/Agency/Sources/Scene/CreateAgency/CreateAgencyVC.swift deleted file mode 100644 index e6cb784b..00000000 --- a/Projects/Feature/Agency/Sources/Scene/CreateAgency/CreateAgencyVC.swift +++ /dev/null @@ -1,156 +0,0 @@ -import UIKit -import Combine - -import DesignSystem -import BaseFeature - -import RxSwift -import RxCocoa -import ReactorKit -import FlexLayout -import PinLayout - -final class CreateAgencyVC: BaseVC, View { - weak var coordinator: AgencyCoordinator? - var disposeBag = DisposeBag() - private var cancelBag = Set() - - private let titleLabel: UILabel = { - let v = UILabel() - v.setTextWithLineHeight(text: "회비 관리가 필요한\n소속 정보를 알려주세요!", lineHeight: 28) - v.numberOfLines = 2 - v.textColor = Colors.Gray._10 - v.font = Fonts.heading._2 - return v - }() - - private let segmentTitleLabel: UILabel = { - let v = UILabel() - v.setTextWithLineHeight(text: "소속 유형", lineHeight: 18) - v.textColor = Colors.Gray._6 - v.font = Fonts.body._2 - return v - }() - - private let agencySegmentControl: MMSegmentControl = { - let v = MMSegmentControl(titles: ["동아리", "학생회", "기타모임"], type: .round) - v.selectedIndex = 0 - return v - }() - - private let agencyTextField: MMTextField = { - let v = MMTextField(charactorLimitCount: 20, title: "소속 이름") - v.setPlaceholder(to: "소속 이름을 입력해주세요.") - return v - }() - - private let createButton: MMButton = MMButton(title: "등록하기", type: .disable) - - override func setupConstraints() { - super.setupConstraints() - - rootContainer.flex.paddingHorizontal(20).define { flex in - flex.addItem(titleLabel).marginBottom(40) - flex.addItem(segmentTitleLabel).marginBottom(8) - flex.addItem(agencySegmentControl).marginBottom(40) - flex.addItem(agencyTextField) - flex.addItem().grow(1) - flex.addItem(createButton).height(56).marginBottom(12) - } - } - - func bind(reactor: CreateAgencyReactor) { - setRightItem(.closeBlack) - - // Action Binding - navigationItem.rightBarButtonItem?.rx.tap - .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) - .bind(with: self) { owner, _ in - owner.coordinator?.present(.alert( - title: "정말 나가시겠습니까?", - subTitle: "입력하신 내용은 저장되지 않습니다.", - okAction: { - owner.coordinator?.dismiss() - }, - cancelAction: { } - )) - } - .disposed(by: disposeBag) - - view.rx.tapGesture - .bind { $0.endEditing(true) } - .disposed(by: disposeBag) - - rx.viewWillAppear - .map { Reactor.Action.onAppear } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - agencyTextField.textField.rx.text - .compactMap { $0 } - .map { Reactor.Action.textFieldDidChange($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - agencyTextField.clearButton.rx.tap - .map { Reactor.Action.textFieldDidChange("") } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - agencySegmentControl.$selectedIndex - .sink { [weak self] in - self?.reactor?.action.onNext(.selectedIndexDidChange($0)) - } - .store(in: &cancelBag) - - createButton.rx.tap - .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) - .map { Reactor.Action.tapCreateButton } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - // State Binding - reactor.pulse(\.$userInfo) - .filter { $0?.universityName == "정보없음"} - .observe(on: MainScheduler.instance) - .bind(with: self) { owner, _ in - owner.agencySegmentControl.selectedIndex = 2 - owner.agencySegmentControl.disableButtons(with: 0,1) - owner.agencySegmentControl.flex.layout() - } - .disposed(by: disposeBag) - - reactor.pulse(\.$isButtonEnabled) - .bind(with: self) { owner, value in - owner.createButton.setState(value ? .primary : .disable) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$destination) - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .bind(with: self) { owner, value in - switch value { - case let .complete(id): - owner.coordinator?.present(.createComplete(id: id)) - } - } - .disposed(by: disposeBag) - - reactor.pulse(\.$error) - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .bind(with: self) { owner, value in - owner.coordinator?.present(.alert( - title: "등록에 실패했습니다", - subTitle: nil, - okAction: { } - )) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$isLoading) - .bind(to: rx.isLoading) - .disposed(by: disposeBag) - } -} diff --git a/Projects/Feature/Base/Sources/Coordinator/Coordinator.swift b/Projects/Feature/Base/Interface/Coordinator.swift similarity index 100% rename from Projects/Feature/Base/Sources/Coordinator/Coordinator.swift rename to Projects/Feature/Base/Interface/Coordinator.swift diff --git a/Projects/Feature/Base/Project.swift b/Projects/Feature/Base/Project.swift index e9f385da..94bbaa34 100644 --- a/Projects/Feature/Base/Project.swift +++ b/Projects/Feature/Base/Project.swift @@ -6,6 +6,9 @@ let project = Project( disableBundleAccessors: true, disableSynthesizedResourceAccessors: true ), + settings: .settings(base: [ + "SWIFT_VERSION": "5.7" + ]), targets: [ Target( name: "BaseFeature", @@ -16,11 +19,19 @@ let project = Project( sources: ["Sources/**"], dependencies: [ .project(target: "DesignSystem", path: .relativeToRoot("Projects/Shared/DesignSystem")), - .project(target: "Core", path: .relativeToRoot("Projects/Core/Core")) - ], - settings: .settings(base: [ - "SWIFT_VERSION": "5.7" - ]) + .project(target: "Core", path: .relativeToRoot("Projects/Core/Core")), + .target(name: "BaseFeatureInterface") + ] + ), + Target( + name: "BaseFeatureInterface", + platform: .iOS, + product: .framework, + bundleId: "com.framework.moneymong.BaseFeatureInterface", + deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), + sources: ["Interface/**"], + dependencies: [ + ] ) ] ) diff --git a/Projects/Feature/CreateAgency/Demo/Resources/LaunchScreen.storyboard b/Projects/Feature/CreateAgency/Demo/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..1f78d48d --- /dev/null +++ b/Projects/Feature/CreateAgency/Demo/Resources/LaunchScreen.storyboard @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Projects/Feature/CreateAgency/Demo/Sources/AppDelegate.swift b/Projects/Feature/CreateAgency/Demo/Sources/AppDelegate.swift new file mode 100644 index 00000000..754c5cae --- /dev/null +++ b/Projects/Feature/CreateAgency/Demo/Sources/AppDelegate.swift @@ -0,0 +1,15 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + + // MARK: UISceneSession Lifecycle + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {} +} diff --git a/Projects/Feature/CreateAgency/Demo/Sources/SceneDelegate.swift b/Projects/Feature/CreateAgency/Demo/Sources/SceneDelegate.swift new file mode 100644 index 00000000..08b5b5f2 --- /dev/null +++ b/Projects/Feature/CreateAgency/Demo/Sources/SceneDelegate.swift @@ -0,0 +1,28 @@ +import UIKit + +import Core +import CreateAgency + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + + let networkManager = NetworkManager() + let localStorage = LocalStorage() + + window?.rootViewController = UINavigationController(rootViewController: InputUniversityInfoFactory(networkManager: networkManager, localStorage: localStorage).make(coordinator: nil, agencyName: "", agencyType: .inSchoolClub)) + window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) {} + + func sceneDidBecomeActive(_ scene: UIScene) {} + + func sceneWillResignActive(_ scene: UIScene) {} + + func sceneWillEnterForeground(_ scene: UIScene) {} + + func sceneDidEnterBackground(_ scene: UIScene) {} +} diff --git a/Projects/Feature/CreateAgency/Interface/CreateAgencyCoordinator.swift b/Projects/Feature/CreateAgency/Interface/CreateAgencyCoordinator.swift new file mode 100644 index 00000000..3f38ac38 --- /dev/null +++ b/Projects/Feature/CreateAgency/Interface/CreateAgencyCoordinator.swift @@ -0,0 +1,36 @@ +import UIKit + +import BaseFeatureInterface + +public final class CreateAgencyCoordinator: Coordinator { + public var navigationController: UINavigationController + public weak var parentCoordinator: Coordinator? + public var childCoordinators: [Coordinator] = [] + + private let inputAgencyFactory: InputAgencyInfoFactoryInterface + + public init( + navigationController: UINavigationController, + inputAgencyFactory: InputAgencyInfoFactoryInterface + ) { + self.navigationController = navigationController + self.inputAgencyFactory = inputAgencyFactory + } + + public func start(animated: Bool, universityType: UniversityType) { + let vc = inputAgencyFactory.make(coordinator: self, universityType: universityType) + navigationController.viewControllers = [vc] + } + + public func move(to scene: Scene) { + switch scene { + case .main: + parentCoordinator?.move(to: .main) + case .ledger: + parentCoordinator?.move(to: .ledger) + case let .createManualLedger(id): + parentCoordinator?.move(to: .createManualLedger(id)) + default: break + } + } +} diff --git a/Projects/Feature/CreateAgency/Interface/CreateCompleteFactoryInterface.swift b/Projects/Feature/CreateAgency/Interface/CreateCompleteFactoryInterface.swift new file mode 100644 index 00000000..59b44fe9 --- /dev/null +++ b/Projects/Feature/CreateAgency/Interface/CreateCompleteFactoryInterface.swift @@ -0,0 +1,5 @@ +import UIKit + +public protocol CreateCompleteFactoryInterface { + func make(coordinator: CreateAgencyCoordinator?, id: Int) -> UIViewController +} diff --git a/Projects/Feature/CreateAgency/Interface/InputAgencyInfoFactoryInterface.swift b/Projects/Feature/CreateAgency/Interface/InputAgencyInfoFactoryInterface.swift new file mode 100644 index 00000000..f1aeb48d --- /dev/null +++ b/Projects/Feature/CreateAgency/Interface/InputAgencyInfoFactoryInterface.swift @@ -0,0 +1,5 @@ +import UIKit + +public protocol InputAgencyInfoFactoryInterface { + func make(coordinator: CreateAgencyCoordinator?, universityType: UniversityType) -> UIViewController +} diff --git a/Projects/Feature/CreateAgency/Interface/InputUniversityInfoFactoryInterface.swift b/Projects/Feature/CreateAgency/Interface/InputUniversityInfoFactoryInterface.swift new file mode 100644 index 00000000..81ec207f --- /dev/null +++ b/Projects/Feature/CreateAgency/Interface/InputUniversityInfoFactoryInterface.swift @@ -0,0 +1,5 @@ +import UIKit + +public protocol InputUniversityInfoFactoryInterface { + func make(coordinator: CreateAgencyCoordinator?, agencyName: String, agencyType: AgencyType) -> UIViewController +} diff --git a/Projects/Feature/CreateAgency/Interface/Model/AgencyType.swift b/Projects/Feature/CreateAgency/Interface/Model/AgencyType.swift new file mode 100644 index 00000000..248384b3 --- /dev/null +++ b/Projects/Feature/CreateAgency/Interface/Model/AgencyType.swift @@ -0,0 +1,5 @@ +public enum AgencyType: String { + case inSchoolClub = "IN_SCHOOL_CLUB" + case studentCouncil = "STUDENT_COUNCIL" + case general = "GENERAL" +} diff --git a/Projects/Feature/CreateAgency/Interface/Model/UniversityType.swift b/Projects/Feature/CreateAgency/Interface/Model/UniversityType.swift new file mode 100644 index 00000000..aab0a609 --- /dev/null +++ b/Projects/Feature/CreateAgency/Interface/Model/UniversityType.swift @@ -0,0 +1,5 @@ +public enum UniversityType { + case none + case exists + case unknown +} diff --git a/Projects/Feature/CreateAgency/Project.swift b/Projects/Feature/CreateAgency/Project.swift new file mode 100644 index 00000000..2f12ae99 --- /dev/null +++ b/Projects/Feature/CreateAgency/Project.swift @@ -0,0 +1,88 @@ +import ProjectDescription + +let project = Project( + name: "CreateAgency", + settings: .settings( + base: .init() + .swiftVersion("5.7") + ), + targets: [ + Target( + name: "CreateAgency", + platform: .iOS, + product: .framework, + bundleId: "com.framework.moneymong.CreateAgency", + deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), + sources: ["Sources/**"], + dependencies: [ + .target(name: "CreateAgencyInterface"), + .project(target: "BaseFeature", path: .relativeToRoot("Projects/Feature/Base")) ] + ), + Target( + name: "CreateAgencyInterface", + platform: .iOS, + product: .framework, + bundleId: "com.framework.moneymong.CreateAgencyInterface", + deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), + sources: ["Interface/**"], + dependencies: [ + .project(target: "BaseFeatureInterface", path: .relativeToRoot("Projects/Feature/Base")) + ] + ), + Target( + name: "CreateAgencyTests", + platform: .iOS, + product: .unitTests, + bundleId: "com.framework.moneymong.CreateAgencyTests", + deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), + sources: ["Tests/**"], + dependencies: [ + .target(name: "CreateAgency"), + .target(name: "CreateAgencyTesting") + ] + ), + Target( + name: "CreateAgencyTesting", + platform: .iOS, + product: .staticLibrary, + bundleId: "com.framework.moneymong.CreateAgencyTesting", + deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), + sources: ["Testing/**"], + dependencies: [ + .target(name: "CreateAgencyInterface") + ] + ), + Target( + name: "CreateAgencyDemo", + platform: .iOS, + product: .app, + bundleId: "com.framework.moneymong.CreateAgencyDemo", + deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), + infoPlist: .extendingDefault(with: [ + "UIUserInterfaceStyle": "Light", + "CFBundleShortVersionString": "1.0", + "CFBundleVersion": "1", + "UILaunchStoryboardName": "LaunchScreen", + "UIApplicationSceneManifest" : [ + "UIApplicationSupportsMultipleScenes":true, + "UISceneConfigurations":[ + "UIWindowSceneSessionRoleApplication":[ + [ + "UISceneConfigurationName":"Default Configuration", + "UISceneDelegateClassName":"$(PRODUCT_MODULE_NAME).SceneDelegate" + ] + ] + ] + ], + "NSLocalNetworkUsageDescription": "Network usage required for debugging purposes", + "NSBonjourServices": ["_pulse._tcp"] + ]), + sources: ["Demo/Sources/**"], + resources: ["Demo/Resources/**"], + dependencies: [ + .target(name: "CreateAgency"), + .target(name: "CreateAgencyTesting") + ] + ) + ] +) diff --git a/Projects/Feature/CreateAgency/Sources/Factorys/CreateCompleteFactory.swift b/Projects/Feature/CreateAgency/Sources/Factorys/CreateCompleteFactory.swift new file mode 100644 index 00000000..bdf503ba --- /dev/null +++ b/Projects/Feature/CreateAgency/Sources/Factorys/CreateCompleteFactory.swift @@ -0,0 +1,23 @@ +import UIKit + +import Core +import CreateAgencyInterface + +public struct CreateCompleteFactory: CreateCompleteFactoryInterface { + + private let networkManager: NetworkManagerInterfacae + private let localStorage: LocalStorageInterface + + public init(networkManager: NetworkManagerInterfacae, localStorage: LocalStorageInterface) { + self.networkManager = networkManager + self.localStorage = localStorage + } + + public func make(coordinator: CreateAgencyCoordinator?, id: Int) -> UIViewController { + let vc = CreateCompleteVC() + vc.coordinator = coordinator + vc.reactor = CreateCompleteReactor(userRepo: UserRepository(networkManager: networkManager, localStorage: localStorage), id: id) + + return vc + } +} diff --git a/Projects/Feature/CreateAgency/Sources/Factorys/InputAgencyInfoFactory.swift b/Projects/Feature/CreateAgency/Sources/Factorys/InputAgencyInfoFactory.swift new file mode 100644 index 00000000..087de805 --- /dev/null +++ b/Projects/Feature/CreateAgency/Sources/Factorys/InputAgencyInfoFactory.swift @@ -0,0 +1,30 @@ +import UIKit + +import Core +import CreateAgencyInterface + +public struct InputAgencyInfoFactory: InputAgencyInfoFactoryInterface { + + private let networkManager: NetworkManagerInterfacae + private let localStorage: LocalStorageInterface + + public init(networkManager: NetworkManagerInterfacae, localStorage: LocalStorageInterface) { + self.networkManager = networkManager + self.localStorage = localStorage + } + + public func make(coordinator: CreateAgencyCoordinator?, universityType: UniversityType) -> UIViewController { + let vc = InputAgencyInfoVC( + createCompleteFactory: CreateCompleteFactory(networkManager: networkManager, localStorage: localStorage), + inputUniversityInfoFactory: InputUniversityInfoFactory(networkManager: networkManager, localStorage: localStorage) + ) + vc.coordinator = coordinator + vc.reactor = InputAgencyInfoReactor( + universityType: universityType, + agencyRepo: AgencyRepository(networkManager: networkManager), + universityRepo: UniversityRepository(networkManager: networkManager) + ) + + return vc + } +} diff --git a/Projects/Feature/CreateAgency/Sources/Factorys/InputUniversityInfoFactory.swift b/Projects/Feature/CreateAgency/Sources/Factorys/InputUniversityInfoFactory.swift new file mode 100644 index 00000000..5bc9fc6a --- /dev/null +++ b/Projects/Feature/CreateAgency/Sources/Factorys/InputUniversityInfoFactory.swift @@ -0,0 +1,27 @@ +import UIKit + +import Core +import CreateAgencyInterface + +public struct InputUniversityInfoFactory: InputUniversityInfoFactoryInterface { + + private let networkManager: NetworkManagerInterfacae + private let localStorage: LocalStorageInterface + + public init(networkManager: NetworkManagerInterfacae, localStorage: LocalStorageInterface) { + self.networkManager = networkManager + self.localStorage = localStorage + } + + public func make(coordinator: CreateAgencyCoordinator?, agencyName: String, agencyType: AgencyType) -> UIViewController { + let vc = InputUniversityInfoVC(completeFactory: CreateCompleteFactory(networkManager: networkManager, localStorage: localStorage)) + vc.coordinator = coordinator + vc.reactor = InputUniversityInfoReactor( + agencyName: agencyName, + agencyType: agencyType, + universityRepository: UniversityRepository(networkManager: networkManager), + agencyRepository: AgencyRepository(networkManager: networkManager) + ) + return vc + } +} diff --git a/Projects/Feature/Agency/Sources/Scene/CreateComplete/CreateCompleteReactor.swift b/Projects/Feature/CreateAgency/Sources/Scene/CreateComplete/CreateCompleteReactor.swift similarity index 100% rename from Projects/Feature/Agency/Sources/Scene/CreateComplete/CreateCompleteReactor.swift rename to Projects/Feature/CreateAgency/Sources/Scene/CreateComplete/CreateCompleteReactor.swift diff --git a/Projects/Feature/Agency/Sources/Scene/CreateComplete/CreateCompleteVC.swift b/Projects/Feature/CreateAgency/Sources/Scene/CreateComplete/CreateCompleteVC.swift similarity index 87% rename from Projects/Feature/Agency/Sources/Scene/CreateComplete/CreateCompleteVC.swift rename to Projects/Feature/CreateAgency/Sources/Scene/CreateComplete/CreateCompleteVC.swift index 7e1c36a0..5a3d1d8d 100644 --- a/Projects/Feature/Agency/Sources/Scene/CreateComplete/CreateCompleteVC.swift +++ b/Projects/Feature/CreateAgency/Sources/Scene/CreateComplete/CreateCompleteVC.swift @@ -2,6 +2,7 @@ import UIKit import BaseFeature import DesignSystem +import CreateAgencyInterface import ReactorKit import RxCocoa @@ -9,7 +10,8 @@ import RxSwift final class CreateCompleteVC: BaseVC, View { var disposeBag = DisposeBag() - weak var coordinator: AgencyCoordinator? + + var coordinator: CreateAgencyCoordinator? private let completeImageView = UIImageView(image: Images.agencyCongrats) private let completeLabel: UILabel = { @@ -46,21 +48,21 @@ final class CreateCompleteVC: BaseVC, View { } func bind(reactor: CreateCompleteReactor) { - reactor.pulse(\.$destination) .observe(on: MainScheduler.instance) .compactMap { $0 } .bind(with: self) { owner, destination in switch destination { case .dismiss: - owner.coordinator?.dismiss() + owner.dismiss(animated: true) + owner.coordinator?.move(to: .main) case .ledger: - owner.coordinator?.dismiss(animated: false) - owner.coordinator?.goLedger() + owner.dismiss(animated: true) + owner.coordinator?.move(to: .ledger) case .manualInput: let id = reactor.currentState.agencyID - owner.coordinator?.dismiss(animated: false) - owner.coordinator?.goManualInput(agencyID: id) + owner.dismiss(animated: true) + owner.coordinator?.move(to: .createManualLedger(id)) } } .disposed(by: disposeBag) diff --git a/Projects/Feature/CreateAgency/Sources/Scene/InputAgencyInfo/InputAgencyInfoReactor.swift b/Projects/Feature/CreateAgency/Sources/Scene/InputAgencyInfo/InputAgencyInfoReactor.swift new file mode 100644 index 00000000..16f72801 --- /dev/null +++ b/Projects/Feature/CreateAgency/Sources/Scene/InputAgencyInfo/InputAgencyInfoReactor.swift @@ -0,0 +1,130 @@ +import Core +import CreateAgencyInterface + +import ReactorKit + +public final class InputAgencyInfoReactor: Reactor { + + public struct State { + @Pulse var agencyType: AgencyType = .inSchoolClub // 소속 종류: 동아리 or 학생회 + @Pulse var text = "" // 소속 이름 + @Pulse var isButtonEnabled = false + + @Pulse var isLoading = false + @Pulse var error: MoneyMongError? + + @Pulse var universityType: UniversityType + + @Pulse var destination: Destination? + + public enum Destination { + case complete(Int) + case inputUniversity(String, AgencyType) + case main + } + } + + public enum Action { + case textFieldDidChange(String) + case selectedIndexDidChange(Int) + case tapCreateButton + case notRegisterButtonDidTap + } + + public enum Mutation { + case setText(String) + case setError(MoneyMongError) + case setLoading(Bool) + case setAgencyType(Int) + case setButtonEnabled(Bool) + case setDestination(State.Destination) + } + + public let initialState: State + private let agencyRepo: AgencyRepositoryInterface + private let universityRepo: UniversityRepositoryInterface + + init( + universityType: UniversityType, + agencyRepo: AgencyRepositoryInterface, + universityRepo: UniversityRepositoryInterface + ) { + self.agencyRepo = agencyRepo + self.initialState = State(universityType: universityType) + self.universityRepo = universityRepo + } + + public func mutate(action: Action) -> Observable { + switch action { + case let .textFieldDidChange(text): + return .concat( + .just(.setText(text)), + .just(.setButtonEnabled((1...20) ~= text.count)) + ) + + case let .selectedIndexDidChange(index): + return .just(.setAgencyType(index)) + + case .tapCreateButton: + if currentState.universityType == .unknown, currentState.agencyType != .general { + return .just(.setDestination(.inputUniversity(currentState.text, currentState.agencyType))) + } else { + return .concat( + .just(.setLoading(true)), + .task { + if currentState.universityType == .unknown { + try await universityRepo.university(name: nil, grade: nil) + } + return try await agencyRepo.create( + name: currentState.text, + type: currentState.agencyType.rawValue + ) + } + .map { .setDestination(.complete($0)) } + .catch { return .just(.setError($0.toMMError)) }, + .just(.setLoading(false)) + ) + } + + case .notRegisterButtonDidTap: + return .task { + if currentState.universityType == .unknown { + try await universityRepo.university(name: nil, grade: nil) + } + } + .map { .setDestination(.main) } + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setText(text): + newState.text = text + case let .setButtonEnabled(value): + newState.isButtonEnabled = value + case let .setAgencyType(index): + newState.agencyType = parsingAgencyType(with: index) ?? .inSchoolClub + case let .setDestination(value): + newState.destination = value + case let .setError(value): + newState.error = value + case let .setLoading(value): + newState.isLoading = value + } + + return newState + } +} + +extension InputAgencyInfoReactor { + func parsingAgencyType(with selectedIndex: Int) -> AgencyType? { + switch selectedIndex { + case 0: .inSchoolClub + case 1: .studentCouncil + case 2: .general + default: nil + } + } +} diff --git a/Projects/Feature/CreateAgency/Sources/Scene/InputAgencyInfo/InputAgencyInfoVC.swift b/Projects/Feature/CreateAgency/Sources/Scene/InputAgencyInfo/InputAgencyInfoVC.swift new file mode 100644 index 00000000..5f20b210 --- /dev/null +++ b/Projects/Feature/CreateAgency/Sources/Scene/InputAgencyInfo/InputAgencyInfoVC.swift @@ -0,0 +1,247 @@ +import UIKit +import Combine + +import DesignSystem +import BaseFeature +import CreateAgencyInterface + +import RxSwift +import RxCocoa +import ReactorKit + +public final class InputAgencyInfoVC: BaseVC, View { + public var disposeBag = DisposeBag() + private var cancelBag = Set() + + private let createCompleteFactory: CreateCompleteFactoryInterface + private let inputUniversityInfoFactory: InputUniversityInfoFactoryInterface + + private var keybordShowCreateButtonConstraints: [NSLayoutConstraint] = [] + private var keybordHideCreateButtonConstraints: [NSLayoutConstraint] = [] + + public var coordinator: CreateAgencyCoordinator? + + init( + createCompleteFactory: CreateCompleteFactoryInterface, + inputUniversityInfoFactory: InputUniversityInfoFactoryInterface + ) { + self.createCompleteFactory = createCompleteFactory + self.inputUniversityInfoFactory = inputUniversityInfoFactory + super.init() + } + + private let titleLabel: UILabel = { + let v = UILabel() + v.setTextWithLineHeight(text: "회비 관리가 필요한\n소속 정보를 알려주세요!", lineHeight: 28) + v.numberOfLines = 2 + v.textColor = Colors.Gray._10 + v.font = Fonts.heading._2 + return v + }() + + private let segmentTitleLabel: UILabel = { + let v = UILabel() + v.setTextWithLineHeight(text: "소속 유형", lineHeight: 18) + v.textColor = Colors.Gray._6 + v.font = Fonts.body._2 + return v + }() + + private let agencySegmentControl: MMSegmentControl = { + let v = MMSegmentControl(titles: ["동아리", "학생회", "기타모임"], type: .round) + v.selectedIndex = 0 + return v + }() + + private let agencyTextField: MMTextField = { + let v = MMTextField(charactorLimitCount: 20, title: "소속 이름") + v.setPlaceholder(to: "소속 이름을 입력해주세요.") + return v + }() + + private let registerButton: MMButton = MMButton(title: "등록하기", type: .disable) + + private let notRegisterButton: UIButton = { + let button = UIButton() + button.setTitle("총무에게 초대받았어요", for: .normal) + button.setTitleColor(Colors.Blue._4, for: .normal) + button.titleLabel?.font = Fonts.body._3 + return button + }() + + public override func setupConstraints() { + super.setupConstraints() + + rootContainer.flex.paddingHorizontal(20).define { flex in + flex.addItem(titleLabel).marginBottom(40) + flex.addItem(segmentTitleLabel).marginBottom(8) + flex.addItem(agencySegmentControl).marginBottom(40) + flex.addItem(agencyTextField) + flex.addItem().grow(1) + } + + view.addSubview(registerButton) + view.addSubview(notRegisterButton) + registerButton.translatesAutoresizingMaskIntoConstraints = false + notRegisterButton.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + notRegisterButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + notRegisterButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + + if reactor?.currentState.universityType != .unknown { + notRegisterButton.isHidden = true + notRegisterButton.heightAnchor.constraint(equalToConstant: 0).isActive = true + } + + keybordHideCreateButtonConstraints = [ + registerButton.heightAnchor.constraint(equalToConstant: 56), + registerButton.bottomAnchor.constraint(equalTo: notRegisterButton.topAnchor, constant: -16), + registerButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12), + registerButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12) + ] + + keybordShowCreateButtonConstraints = [ + registerButton.heightAnchor.constraint(equalToConstant: 56), + registerButton.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), + registerButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 3), + registerButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: -3) + ] + + NSLayoutConstraint.activate(keybordHideCreateButtonConstraints) + } + + public func bind(reactor: InputAgencyInfoReactor) { + // Action Binding + setRightItem(.closeBlack) + + NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) + .bind(with: self) { owner, _ in + UIView.animate(withDuration: 0.2) { + NSLayoutConstraint.deactivate(owner.keybordHideCreateButtonConstraints) + NSLayoutConstraint.activate(owner.keybordShowCreateButtonConstraints) + } + + UIView.animate(withDuration: 0.2) { + owner.registerButton.layer.cornerRadius = 0 + } + owner.view.layoutIfNeeded() + } + .disposed(by: disposeBag) + + NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) + .bind(with: self) { owner, _ in + UIView.animate(withDuration: 0.2) { + NSLayoutConstraint.deactivate(owner.keybordShowCreateButtonConstraints) + NSLayoutConstraint.activate(owner.keybordHideCreateButtonConstraints) + } + + UIView.animate(withDuration: 0.2) { + owner.registerButton.layer.cornerRadius = 12 + } + owner.view.layoutIfNeeded() + } + .disposed(by: disposeBag) + + navigationItem.rightBarButtonItem?.rx.tap + .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) + .bind(with: self) { owner, _ in + AlertsManager.show( + title: "정말 나가시겠습니까?", + subTitle: "입력하신 내용은 저장되지 않습니다.", + type: .default(okAction: { + owner.dismiss(animated: true) + }, cancelAction: { + + }) + ) + } + .disposed(by: disposeBag) + + view.rx.tapGesture + .bind { $0.endEditing(true) } + .disposed(by: disposeBag) + + agencyTextField.textField.rx.text + .compactMap { $0 } + .map { Reactor.Action.textFieldDidChange($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + agencyTextField.clearButton.rx.tap + .map { Reactor.Action.textFieldDidChange("") } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + agencySegmentControl.$selectedIndex + .sink { [weak self] in + self?.reactor?.action.onNext(.selectedIndexDidChange($0)) + } + .store(in: &cancelBag) + + registerButton.rx.tap + .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) + .map { Reactor.Action.tapCreateButton } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + notRegisterButton.rx.tap + .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) + .map { Reactor.Action.notRegisterButtonDidTap } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + // State Binding + reactor.pulse(\.$universityType) + .filter { $0 == .none} + .observe(on: MainScheduler.instance) + .bind(with: self) { owner, _ in + owner.agencySegmentControl.selectedIndex = 2 + owner.agencySegmentControl.disableButtons(with: 0,1) + owner.agencySegmentControl.flex.layout() + } + .disposed(by: disposeBag) + + reactor.pulse(\.$isButtonEnabled) + .bind(with: self) { owner, value in + owner.registerButton.setState(value ? .primary : .disable) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$destination) + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .bind(with: self) { owner, value in + guard let coordinator = owner.coordinator else { return } + switch value { + case let .complete(id): + let vc = owner.createCompleteFactory.make(coordinator: coordinator, id: id) + owner.navigationController?.pushViewController(vc, animated: true) + case let .inputUniversity(agencyName, agencyType): + let vc = owner.inputUniversityInfoFactory.make(coordinator: coordinator, agencyName: agencyName, agencyType: agencyType) + owner.navigationController?.pushViewController(vc, animated: true) + case .main: + owner.dismiss(animated: true) + owner.coordinator?.move(to: .main) + } + } + .disposed(by: disposeBag) + + reactor.pulse(\.$error) + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .bind(with: self) { owner, value in + AlertsManager.show( + title: "등록에 실패했습니다", + subTitle: nil, + type: .onlyOkButton({}) + ) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$isLoading) + .bind(to: rx.isLoading) + .disposed(by: disposeBag) + } +} diff --git a/Projects/Feature/Sign/Sources/Scene/SignUp/Components/EmptyListView.swift b/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/Components/EmptyListView.swift similarity index 100% rename from Projects/Feature/Sign/Sources/Scene/SignUp/Components/EmptyListView.swift rename to Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/Components/EmptyListView.swift diff --git a/Projects/Feature/Sign/Sources/Scene/SignUp/Components/GradeInputView.swift b/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/Components/GradeInputView.swift similarity index 100% rename from Projects/Feature/Sign/Sources/Scene/SignUp/Components/GradeInputView.swift rename to Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/Components/GradeInputView.swift diff --git a/Projects/Feature/Sign/Sources/Scene/SignUp/Components/UniversityCell.swift b/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/Components/UniversityCell.swift similarity index 100% rename from Projects/Feature/Sign/Sources/Scene/SignUp/Components/UniversityCell.swift rename to Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/Components/UniversityCell.swift diff --git a/Projects/Feature/Sign/Sources/Scene/SignUp/SignUpReactor.swift b/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/InputUniversityInfoReactor.swift similarity index 64% rename from Projects/Feature/Sign/Sources/Scene/SignUp/SignUpReactor.swift rename to Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/InputUniversityInfoReactor.swift index 83d283a9..705e2ae6 100644 --- a/Projects/Feature/Sign/Sources/Scene/SignUp/SignUpReactor.swift +++ b/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/InputUniversityInfoReactor.swift @@ -1,16 +1,15 @@ import Core +import CreateAgencyInterface import ReactorKit -final class SignUpReactor: Reactor { +final class InputUniversityInfoReactor: Reactor { enum Action { case searchKeyword(String) case selectUniversity(University) - case unSelectUniversity - case selectGrade(Int) case confirm - case notUnivercityInfo + case notRegisterButtonDidTap } enum Mutation { @@ -18,20 +17,9 @@ final class SignUpReactor: Reactor { case setEmptyList(Bool) case setIsLoading(Bool) case setErrorMessage(String) - case setInputType(InputType) case setIsConfirm(Bool) - case setDestination(Destination) - case setSelectedUniversity(University) - case setSelectedGrade(Int) - } - - enum InputType { - case university - case grade(University) - } - - enum Destination { - case congratulations + case setDestination(State.Destination) + case setSelectedUniversity(University?) } struct State { @@ -40,23 +28,40 @@ final class SignUpReactor: Reactor { @Pulse var errorMessage: String? @Pulse var schoolList: [University]? @Pulse var isEmptyList: Bool? - @Pulse var inputType: InputType = .university - @Pulse var destination: Destination? - var selectedUniversity: University? - var selectedGrade: Int? + @Pulse var destination: State.Destination? + @Pulse var agencyName: String + @Pulse var agencyType: AgencyType + @Pulse var selectedUniversity: University? + + enum Destination { + case main + case complete(Int) + } } - let initialState: State = State() + let initialState: State + private let universityRepository: UniversityRepositoryInterface - - init(universityRepository: UniversityRepositoryInterface) { + private let agencyRepository: AgencyRepositoryInterface + + init( + agencyName: String, + agencyType: AgencyType, + universityRepository: UniversityRepositoryInterface, + agencyRepository: AgencyRepositoryInterface + ) { self.universityRepository = universityRepository + self.agencyRepository = agencyRepository + self.initialState = State(agencyName: agencyName, agencyType: agencyType) } func mutate(action: Action) -> Observable { switch action { case .searchKeyword(let keyword): return Observable.create { [unowned self] observer in + observer.onNext(.setSelectedUniversity(nil)) + observer.onNext(.setIsConfirm(false)) + if keyword == "" { observer.onNext(.setSchoolList([])) observer.onNext(.setEmptyList(false)) @@ -78,58 +83,37 @@ final class SignUpReactor: Reactor { } case .selectUniversity(let university): - return Observable.concat([ + return .concat([ .just(.setSelectedUniversity(university)), - .just(.setInputType(.grade(university))) - ]) - - case .unSelectUniversity: - return Observable.concat([ - .just(.setIsConfirm(false)), - .just(.setInputType(.university)) + .just(.setIsConfirm(true)) ]) - case .selectGrade(let grade): - return Observable.create { [unowned self] observer in - observer.onNext(.setSelectedGrade(grade)) - - if currentState.selectedUniversity != nil, - let grade = currentState.selectedGrade, - (1...5) ~= grade - { - observer.onNext(.setIsConfirm(true)) - } else { - observer.onNext(.setIsConfirm(false)) - } - return Disposables.create() - } - case .confirm: return Observable.concat([ .just(.setIsLoading(true)), .task { [unowned self] in - guard let university = currentState.selectedUniversity, - let grade = currentState.selectedGrade else { + guard let university = currentState.selectedUniversity else { throw MoneyMongError.appError(.default, errorMessage: "필수 입력값을 입력해주세요.") } - return try await universityRepository.university( + try await universityRepository.university( name: university.schoolName, - grade: grade + grade: nil ) + + return try await agencyRepository.create(name: currentState.agencyName, type: currentState.agencyType.rawValue) } - .map {.setDestination(.congratulations) } + .map { .setDestination(.complete($0)) } .catch { error in .just(.setErrorMessage(error.localizedDescription)) }, - .just(.setIsLoading(false)) ]) - case .notUnivercityInfo: + case .notRegisterButtonDidTap: return Observable.concat([ .just(.setIsLoading(true)), .task { [unowned self] in return try await universityRepository.university(name: nil, grade: nil) } - .map {.setDestination(.congratulations) } + .map {.setDestination(.main) } .catch { error in .just(.setErrorMessage(error.localizedDescription)) }, .just(.setIsLoading(false)) @@ -146,8 +130,6 @@ final class SignUpReactor: Reactor { newState.errorMessage = errorMessage case .setSchoolList(let list): newState.schoolList = list - case .setInputType(let type): - newState.inputType = type case .setIsConfirm(let value): newState.isConfirm = value case .setDestination(let destination): @@ -156,8 +138,6 @@ final class SignUpReactor: Reactor { newState.isEmptyList = value case .setSelectedUniversity(let university): newState.selectedUniversity = university - case .setSelectedGrade(let grade): - newState.selectedGrade = grade } return newState } diff --git a/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/InputUniversityInfoVC.swift b/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/InputUniversityInfoVC.swift new file mode 100644 index 00000000..d713d391 --- /dev/null +++ b/Projects/Feature/CreateAgency/Sources/Scene/InputUniversityInfo/InputUniversityInfoVC.swift @@ -0,0 +1,286 @@ +import UIKit +import Combine + +import DesignSystem +import BaseFeature +import Core +import CreateAgencyInterface + +import FlexLayout +import PinLayout +import ReactorKit + +final class InputUniversityInfoVC: UIViewController, View { + var disposeBag = DisposeBag() + private var anyCancellable = Set() + + private let completeFactory: CreateCompleteFactoryInterface + + var coordinator: CreateAgencyCoordinator? + + private var keybordShowCreateButtonConstraints: [NSLayoutConstraint] = [] + private var keybordHideCreateButtonConstraints: [NSLayoutConstraint] = [] + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = Const.title + label.font = Fonts.heading._2 + label.textColor = Colors.Black._1 + return label + }() + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.text = Const.description + label.font = Fonts.body._3 + label.textColor = Colors.Gray._6 + return label + }() + + private let searchBar: MMSearchBar = { + let searchBar = MMSearchBar(title: Const.university, didSearch: nil) + searchBar.setPlaceholder(to: Const.searchBarPlaceholder) + return searchBar + }() + + private let emptyListView: EmptyListView = { + let view = EmptyListView() + view.isHidden = true + return view + }() + + private let tableView: UITableView = { + let tableView = UITableView() + tableView.register(UniversityCell.self) + tableView.keyboardDismissMode = .interactive + tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false + tableView.contentInset.bottom = 28 + return tableView + }() + + private let registerButton: MMButton = { + let button = MMButton(title: Const.confirmTitle, type: .disable) + return button + }() + + private let notRegisterButton: UIButton = { + let button = UIButton() + button.setTitle(Const.notRegisterButton, for: .normal) + button.setTitleColor(Colors.Blue._4, for: .normal) + button.titleLabel?.font = Fonts.body._3 + return button + }() + + init(completeFactory: CreateCompleteFactoryInterface) { + self.completeFactory = completeFactory + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupConstraints() + setupUI() + } + + func setupUI() { + view.backgroundColor = .white + navigationItem.hidesBackButton = true + tableView.backgroundView = emptyListView + } + + func setupConstraints() { + view.addSubview(titleLabel) + view.addSubview(descriptionLabel) + view.addSubview(searchBar) + view.addSubview(tableView) + view.addSubview(registerButton) + view.addSubview(notRegisterButton) + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + searchBar.translatesAutoresizingMaskIntoConstraints = false + tableView.translatesAutoresizingMaskIntoConstraints = false + registerButton.translatesAutoresizingMaskIntoConstraints = false + notRegisterButton.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12), + titleLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + titleLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20) + ]) + + NSLayoutConstraint.activate([ + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + descriptionLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + descriptionLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20) + ]) + + NSLayoutConstraint.activate([ + searchBar.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 40), + searchBar.heightAnchor.constraint(equalToConstant: 56), + searchBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + searchBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20) + ]) + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 4), + tableView.bottomAnchor.constraint(equalTo: registerButton.topAnchor), + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20) + ]) + + keybordHideCreateButtonConstraints = [ + registerButton.heightAnchor.constraint(equalToConstant: 56), + registerButton.bottomAnchor.constraint(equalTo: notRegisterButton.topAnchor, constant: -16), + registerButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12), + registerButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12) + ] + + keybordShowCreateButtonConstraints = [ + registerButton.heightAnchor.constraint(equalToConstant: 56), + registerButton.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), + registerButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 3), + registerButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: -3) + ] + + NSLayoutConstraint.activate(keybordHideCreateButtonConstraints) + + NSLayoutConstraint.activate([ + notRegisterButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + notRegisterButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + } + + func bind(reactor: InputUniversityInfoReactor) { + // State Binding + + reactor.pulse(\.$errorMessage) + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .bind(with: self) { owner, errorMessage in + AlertsManager.show(title: errorMessage, type: .onlyOkButton()) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$schoolList) + .compactMap { $0 } + .bind(to: tableView.rx.items ( + cellIdentifier: UniversityCell.reuseIdentifier, + cellType: UniversityCell.self + )) { row, item, cell in + cell.configure(with: item) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$isEmptyList) + .compactMap { $0 }.map { !$0 } + .observe(on: MainScheduler.instance) + .bind(to: emptyListView.rx.isHidden) + .disposed(by: disposeBag) + + reactor.pulse(\.$isConfirm) + .observe(on: MainScheduler.instance) + .bind(with: self) { owner, value in + owner.registerButton.setState(value ? .primary : .disable) + } + .disposed(by: disposeBag) + + reactor.pulse(\.$destination) + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .bind(with: self) { owner, destination in + switch destination { + case .main: + owner.dismiss(animated: true) + owner.coordinator?.move(to: .main) + case let .complete(id): + let vc = owner.completeFactory.make(coordinator: owner.coordinator, id: id) + owner.navigationController?.pushViewController(vc, animated: true) + } + } + .disposed(by: disposeBag) + + // Action Binding + + setLeftItem(.back) + + NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) + .bind(with: self) { owner, _ in + UIView.animate(withDuration: 0.2) { + NSLayoutConstraint.deactivate(owner.keybordHideCreateButtonConstraints) + NSLayoutConstraint.activate(owner.keybordShowCreateButtonConstraints) + } + + UIView.animate(withDuration: 0.2) { + owner.registerButton.layer.cornerRadius = 0 + } + owner.view.layoutIfNeeded() + } + .disposed(by: disposeBag) + + NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) + .bind(with: self) { owner, _ in + UIView.animate(withDuration: 0.2) { + NSLayoutConstraint.deactivate(owner.keybordShowCreateButtonConstraints) + NSLayoutConstraint.activate(owner.keybordHideCreateButtonConstraints) + } + + UIView.animate(withDuration: 0.2) { + owner.registerButton.layer.cornerRadius = 12 + } + owner.view.layoutIfNeeded() + } + .disposed(by: disposeBag) + + navigationItem.leftBarButtonItem?.rx.tap + .bind(with: self) { owner, _ in + owner.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + + view.rx.tapGesture + .bind { $0.endEditing(true) } + .disposed(by: disposeBag) + + searchBar.textField.rx.text + .orEmpty + .distinctUntilChanged() + .debounce(.milliseconds(1500), scheduler: MainScheduler.instance) + .map { Reactor.Action.searchKeyword($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + tableView.rx.modelSelected(University.self) + .bind(with: self) { owner, item in + reactor.action.onNext(.selectUniversity(item)) + } + .disposed(by: disposeBag) + + registerButton.rx.tap + .throttle(.milliseconds(500), scheduler: MainScheduler.instance) + .map { Reactor.Action.confirm } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + notRegisterButton.rx.tap + .throttle(.milliseconds(500), scheduler: MainScheduler.instance) + .map { Reactor.Action.notRegisterButtonDidTap } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } +} + +fileprivate enum Const { + static var title: String { "어디 학교 교내 동아리인가요?" } + static var description: String { "소속 대학교를 알려주세요" } + static var confirmTitle: String { "등록하기" } + static var university: String { "대학교" } + static var searchBarPlaceholder: String { "ex)머니대학교" } + static var notRegisterButton: String { "총무에게 초대받았어요" } +} diff --git a/Projects/Feature/CreateAgency/Testing/Dummy.swift b/Projects/Feature/CreateAgency/Testing/Dummy.swift new file mode 100644 index 00000000..9d3c9e47 --- /dev/null +++ b/Projects/Feature/CreateAgency/Testing/Dummy.swift @@ -0,0 +1 @@ +// Dummy \ No newline at end of file diff --git a/Projects/Feature/CreateAgency/Tests/CreateAgencyTests.swift b/Projects/Feature/CreateAgency/Tests/CreateAgencyTests.swift new file mode 100644 index 00000000..81922357 --- /dev/null +++ b/Projects/Feature/CreateAgency/Tests/CreateAgencyTests.swift @@ -0,0 +1,13 @@ +import XCTest +@testable import CreateAgency + +final class CreateAgencyTests: XCTestCase { + + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() throws { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Feature/Ledger/Sources/Common/DIContainer/LedgerDIContainer.swift b/Projects/Feature/Ledger/Sources/Common/DIContainer/LedgerDIContainer.swift index e34cb3a5..bb1c0025 100644 --- a/Projects/Feature/Ledger/Sources/Common/DIContainer/LedgerDIContainer.swift +++ b/Projects/Feature/Ledger/Sources/Common/DIContainer/LedgerDIContainer.swift @@ -1,7 +1,7 @@ import UIKit import Core -import BaseFeature +import BaseFeatureInterface public final class LedgerDIContainer { diff --git a/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/ImagePickerPresentable.swift b/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/ImagePickerPresentable.swift index fd7d9ba4..cf47347d 100644 --- a/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/ImagePickerPresentable.swift +++ b/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/ImagePickerPresentable.swift @@ -1,6 +1,6 @@ import UIKit -import BaseFeature +import BaseFeatureInterface protocol ImagePickerPresentable where Self: Coordinator { func imagePicker( diff --git a/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/LedgerCoordinator.swift b/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/LedgerCoordinator.swift index faf0e1f0..fc5febde 100644 --- a/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/LedgerCoordinator.swift +++ b/Projects/Feature/Ledger/Sources/Common/LedgerCoordinator/LedgerCoordinator.swift @@ -2,7 +2,7 @@ import UIKit import DesignSystem import Core -import BaseFeature +import BaseFeatureInterface public final class LedgerCoordinator: Coordinator { public var navigationController: UINavigationController diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Manual/CreateManualLedgerCoordinator.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Manual/CreateManualLedgerCoordinator.swift index 0d050291..a6ea27cb 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Manual/CreateManualLedgerCoordinator.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Manual/CreateManualLedgerCoordinator.swift @@ -1,6 +1,6 @@ import UIKit -import BaseFeature +import BaseFeatureInterface import DesignSystem final class CreateManualLedgerCoordinator: Coordinator { diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerCoordinator.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerCoordinator.swift index 3d4f0df6..672a4e99 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerCoordinator.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerCoordinator.swift @@ -1,6 +1,6 @@ import UIKit -import BaseFeature +import BaseFeatureInterface import DesignSystem import Core diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerDIContainer.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerDIContainer.swift index b2f30213..a6aa1f76 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerDIContainer.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/Creaters/Scan/CreateOCRLedgerDIContainer.swift @@ -1,6 +1,6 @@ import UIKit -import BaseFeature +import BaseFeatureInterface import Core final class CreateOCRLedgerDIContainer { diff --git a/Projects/Feature/Main/Project.swift b/Projects/Feature/Main/Project.swift index 772a7c7f..6540e097 100644 --- a/Projects/Feature/Main/Project.swift +++ b/Projects/Feature/Main/Project.swift @@ -16,9 +16,9 @@ let project = Project( sources: ["Sources/**"], dependencies: [ .project(target: "BaseFeature", path: .relativeToRoot("Projects/Feature/Base")), - .project(target: "MyPageFeature", path: .relativeToRoot("Projects/Feature/MyPage")), - .project(target: "AgencyFeature", path: .relativeToRoot("Projects/Feature/Agency")), - .project(target: "LedgerFeature", path: .relativeToRoot("Projects/Feature/Ledger")) + .project(target: "MyPageFeature", path: .relativeToRoot("Projects/Feature/MyPage")), + .project(target: "AgencyFeature", path: .relativeToRoot("Projects/Feature/Agency")), + .project(target: "LedgerFeature", path: .relativeToRoot("Projects/Feature/Ledger")) ], settings: .settings(base: [ "SWIFT_VERSION": "5.7" diff --git a/Projects/Feature/Main/Sources/MainDIContainer.swift b/Projects/Feature/Main/Sources/MainDIContainer.swift index 6537f1f5..01cb7d62 100644 --- a/Projects/Feature/Main/Sources/MainDIContainer.swift +++ b/Projects/Feature/Main/Sources/MainDIContainer.swift @@ -1,10 +1,12 @@ import UIKit -import BaseFeature import AgencyFeature +import BaseFeatureInterface +import Core +import CreateAgencyInterface import LedgerFeature import MyPageFeature -import Core + public final class MainDIContainer { private let localStorage: LocalStorageInterface @@ -13,17 +15,19 @@ public final class MainDIContainer { private let agencyContainer: AgencyDIContainer private let myPageContainer: MyPageDIContainer private let ledgerContainer: LedgerDIContainer - + public init( localStorage: LocalStorageInterface, - networkManager: NetworkManagerInterfacae + networkManager: NetworkManagerInterfacae, + inputAgencyInfoFactory: InputAgencyInfoFactoryInterface ) { self.localStorage = localStorage self.networkManager = networkManager - + self.agencyContainer = .init( localStorage: localStorage, - networkManager: networkManager + networkManager: networkManager, + inputAgencyInfoFactory: inputAgencyInfoFactory ) self.myPageContainer = .init( diff --git a/Projects/Feature/Main/Sources/MainTabBarCoordinator.swift b/Projects/Feature/Main/Sources/MainTabBarCoordinator.swift index 2e77a5c5..e42fd9a1 100644 --- a/Projects/Feature/Main/Sources/MainTabBarCoordinator.swift +++ b/Projects/Feature/Main/Sources/MainTabBarCoordinator.swift @@ -1,6 +1,6 @@ import UIKit -import BaseFeature +import BaseFeatureInterface import LedgerFeature public final class MainTabBarCoordinator: Coordinator { diff --git a/Projects/Feature/Main/Sources/MainTapViewController.swift b/Projects/Feature/Main/Sources/MainTapViewController.swift index a5931beb..0f69bfb3 100644 --- a/Projects/Feature/Main/Sources/MainTapViewController.swift +++ b/Projects/Feature/Main/Sources/MainTapViewController.swift @@ -1,6 +1,6 @@ import UIKit -import BaseFeature +import BaseFeatureInterface import Core import DesignSystem diff --git a/Projects/Feature/MyPage/Sources/Common/MyPageCoordinator.swift b/Projects/Feature/MyPage/Sources/Common/MyPageCoordinator.swift index ee30af34..e92e7fca 100644 --- a/Projects/Feature/MyPage/Sources/Common/MyPageCoordinator.swift +++ b/Projects/Feature/MyPage/Sources/Common/MyPageCoordinator.swift @@ -1,7 +1,7 @@ import UIKit import SwiftUI -import BaseFeature +import BaseFeatureInterface import DesignSystem public final class MyPageCoordinator: Coordinator { diff --git a/Projects/Feature/MyPage/Sources/Scene/MyPage/Cell/UniversityCell.swift b/Projects/Feature/MyPage/Sources/Scene/MyPage/Cell/UniversityCell.swift index abedfbba..a40d4e50 100644 --- a/Projects/Feature/MyPage/Sources/Scene/MyPage/Cell/UniversityCell.swift +++ b/Projects/Feature/MyPage/Sources/Scene/MyPage/Cell/UniversityCell.swift @@ -75,19 +75,8 @@ final class UniversityCell: UITableViewCell, ReusableView { switch item { case let .university(userInfo): - let universityText: String - - // 대학 정보가 없는경우 - if userInfo.grade == 0 { - universityText = "정보 없음" - } - // 대학 정보가 있는 경우 - else { - universityText = "\(userInfo.universityName) \(userInfo.grade)학년 \(userInfo.grade == 5 ? "이상" : "")" - } - universityLabel.setTextWithLineHeight( - text: universityText, + text: userInfo.universityName, lineHeight: 24 ) universityLabel.flex.markDirty() diff --git a/Projects/Feature/Sign/Project.swift b/Projects/Feature/Sign/Project.swift index 235999bb..ea415634 100644 --- a/Projects/Feature/Sign/Project.swift +++ b/Projects/Feature/Sign/Project.swift @@ -24,7 +24,8 @@ let project = Project( ]), sources: ["Sources/**"], dependencies: [ - .project(target: "BaseFeature", path: .relativeToRoot("Projects/Feature/Base")) + .project(target: "BaseFeature", path: .relativeToRoot("Projects/Feature/Base")), + .project(target: "CreateAgencyInterface", path: .relativeToRoot("Projects/Feature/CreateAgency")) ], settings: .settings(base: [ "SWIFT_VERSION": "5.7" diff --git a/Projects/Feature/Sign/Sources/Scene/Common/SignCoordinator.swift b/Projects/Feature/Sign/Sources/Scene/Common/SignCoordinator.swift index d5151109..468cca29 100644 --- a/Projects/Feature/Sign/Sources/Scene/Common/SignCoordinator.swift +++ b/Projects/Feature/Sign/Sources/Scene/Common/SignCoordinator.swift @@ -1,6 +1,7 @@ import UIKit -import BaseFeature +import BaseFeatureInterface +import CreateAgencyInterface import DesignSystem public final class SignCoordinator: Coordinator { @@ -8,7 +9,7 @@ public final class SignCoordinator: Coordinator { private let diContainer: SignDIContainer public weak var parentCoordinator: Coordinator? public var childCoordinators: [Coordinator] = [] - + public init(navigationController: UINavigationController, diContainer: SignDIContainer) { self.navigationController = navigationController self.diContainer = diContainer @@ -18,6 +19,21 @@ public final class SignCoordinator: Coordinator { splash() } + public func move(to scene: Scene) { + switch scene { + case .main: + parentCoordinator?.move(to: .main) + remove() + case .ledger: + parentCoordinator?.move(to: .ledger) + remove() + case .createManualLedger(let id): + parentCoordinator?.move(to: .createManualLedger(id)) + remove() + default: break + } + } + deinit { debugPrint(#function) } @@ -40,9 +56,10 @@ public extension SignCoordinator { remove() } - func signUp(animated: Bool = true) { - let vc = diContainer.signUp(with: self) - navigationController.pushViewController(vc, animated: animated) + func createAgency(animated: Bool = true) { + let vc = diContainer.createAgency(with: self) + vc.modalPresentationStyle = .fullScreen + navigationController.present(vc, animated: animated) } func congratulations(animated: Bool = true) { diff --git a/Projects/Feature/Sign/Sources/Scene/Common/SignDIContainer.swift b/Projects/Feature/Sign/Sources/Scene/Common/SignDIContainer.swift index 7fd8263a..d5639a33 100644 --- a/Projects/Feature/Sign/Sources/Scene/Common/SignDIContainer.swift +++ b/Projects/Feature/Sign/Sources/Scene/Common/SignDIContainer.swift @@ -1,15 +1,22 @@ +import UIKit + import Core +import CreateAgencyInterface public final class SignDIContainer { private let localStorage: LocalStorageInterface private let networkManager: NetworkManagerInterfacae + + private let inputAgencyInfoFactory: InputAgencyInfoFactoryInterface public init( - localStorage: LocalStorageInterface = LocalStorage(), - networkManager: NetworkManagerInterfacae + localStorage: LocalStorageInterface, + networkManager: NetworkManagerInterfacae, + inputAgencyInfoFactory: InputAgencyInfoFactoryInterface ) { self.localStorage = localStorage self.networkManager = networkManager + self.inputAgencyInfoFactory = inputAgencyInfoFactory } func splash(with coordinator: SignCoordinator) -> SplashVC { @@ -46,12 +53,12 @@ public final class SignDIContainer { return vc } - func signUp(with coordinator: SignCoordinator) -> SignUpVC { - let vc = SignUpVC() - let universityRepository = UniversityRepository(networkManager: networkManager) - vc.reactor = SignUpReactor(universityRepository: universityRepository) - vc.coordinator = coordinator - return vc + func createAgency(with coordinator: SignCoordinator) -> UIViewController { + let navigationController = UINavigationController() + let createAgencyCoordinator = CreateAgencyCoordinator(navigationController: navigationController, inputAgencyFactory: inputAgencyInfoFactory) + createAgencyCoordinator.parentCoordinator = coordinator + createAgencyCoordinator.start(animated: true, universityType: .unknown) + return navigationController } func congratulations(with coordinator: SignCoordinator) -> CongratulationsVC { diff --git a/Projects/Feature/Sign/Sources/Scene/Login/LoginVC.swift b/Projects/Feature/Sign/Sources/Scene/Login/LoginVC.swift index dc248afc..4149e26e 100644 --- a/Projects/Feature/Sign/Sources/Scene/Login/LoginVC.swift +++ b/Projects/Feature/Sign/Sources/Scene/Login/LoginVC.swift @@ -113,7 +113,7 @@ final class LoginVC: BaseVC, View { case .main: owner.coordinator?.main() case .signUp: - owner.coordinator?.signUp() + owner.coordinator?.createAgency() } } .disposed(by: disposeBag) diff --git a/Projects/Feature/Sign/Sources/Scene/SignUp/SignUpVC.swift b/Projects/Feature/Sign/Sources/Scene/SignUp/SignUpVC.swift deleted file mode 100644 index 4e92871d..00000000 --- a/Projects/Feature/Sign/Sources/Scene/SignUp/SignUpVC.swift +++ /dev/null @@ -1,251 +0,0 @@ -import UIKit -import Combine - -import DesignSystem -import BaseFeature -import Core - -import ReactorKit - -final class SignUpVC: BaseVC, View { - - weak var coordinator: SignCoordinator? - var disposeBag = DisposeBag() - private var anyCancellable = Set() - - private let titleLabel: UILabel = { - let label = UILabel() - label.text = Const.title - label.font = Fonts.heading._2 - label.textColor = Colors.Black._1 - return label - }() - - private let descriptionLabel: UILabel = { - let label = UILabel() - label.text = Const.description - label.font = Fonts.body._3 - label.textColor = Colors.Gray._6 - return label - }() - - private let searchBar: MMSearchBar = { - let searchBar = MMSearchBar(title: Const.university, didSearch: nil) - searchBar.setPlaceholder(to: Const.searchBarPlaceholder) - return searchBar - }() - - private let emptyListView: EmptyListView = { - let view = EmptyListView() - view.isHidden = true - return view - }() - - private let tableView: UITableView = { - let tableView = UITableView() - tableView.register(UniversityCell.self) - tableView.keyboardDismissMode = .interactive - tableView.separatorStyle = .none - tableView.showsVerticalScrollIndicator = false - return tableView - }() - - private let gradeInputView: GradeInputView = { - let view = GradeInputView() - return view - }() - - private let confirmButton: MMButton = { - let button = MMButton(title: Const.confirmTitle, type: .disable) - return button - }() - - private let notUniversityInfoButton: UIButton = { - let button = UIButton() - button.setTitle(Const.universityInfoEmpty, for: .normal) - button.setTitleColor(Colors.Blue._4, for: .normal) - button.titleLabel?.font = Fonts.body._3 - return button - }() - - override func setupUI() { - super.setupUI() - - tableView.backgroundView = emptyListView - } - - private let searchContentView = UIView() - override func setupConstraints() { - super.setupConstraints() - rootContainer.flex - .backgroundColor(Colors.White._1) - .paddingHorizontal(20) - .define { flex in - flex.addItem().height(12) - flex.addItem(titleLabel).marginBottom(8) - flex.addItem(descriptionLabel).marginBottom(40) - - flex.addItem(searchContentView).grow(1).define { flex in - flex.addItem(searchBar).marginBottom(4) - flex.addItem(tableView).grow(1) - } - - flex.addItem(gradeInputView).grow(1) - flex.addItem(confirmButton).height(56).marginBottom(16) - flex.addItem(notUniversityInfoButton).marginBottom(12) - } - } - - func bind(reactor: SignUpReactor) { - // State Binding - - reactor.pulse(\.$errorMessage) - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .bind(with: self) { owner, errorMessage in - owner.coordinator?.alert(title: errorMessage) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$isLoading) - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .bind(to: rx.isLoading) - .disposed(by: disposeBag) - - reactor.pulse(\.$schoolList) - .compactMap { $0 } - .bind(to: tableView.rx.items ( - cellIdentifier: UniversityCell.reuseIdentifier, - cellType: UniversityCell.self - )) { row, item, cell in - cell.configure(with: item) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$isEmptyList) - .compactMap { $0 }.map { !$0 } - .observe(on: MainScheduler.instance) - .bind(to: emptyListView.rx.isHidden) - .disposed(by: disposeBag) - - reactor.pulse(\.$inputType) - .observe(on: MainScheduler.instance) - .bind(with: self) { owner, inputType in - switch inputType { - case .university: - owner.setUniversityInput() - case .grade(let university): - owner.setGradeInput(to: university) - } - } - .disposed(by: disposeBag) - - reactor.pulse(\.$isConfirm) - .observe(on: MainScheduler.instance) - .bind(with: self) { owner, value in - owner.confirmButton.setState(value ? .primary : .disable) - } - .disposed(by: disposeBag) - - reactor.pulse(\.$destination) - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .bind(with: self) { owner, destination in - switch destination { - case .congratulations: - owner.coordinator?.congratulations() - } - } - .disposed(by: disposeBag) - - // Action Binding - - setLeftItem(.back) - - navigationItem.leftBarButtonItem?.rx.tap - .bind(with: self) { owner, _ in - owner.coordinator?.pop() - } - .disposed(by: disposeBag) - - view.rx.tapGesture - .bind { $0.endEditing(true) } - .disposed(by: disposeBag) - - searchBar.textField.rx.text - .orEmpty - .distinctUntilChanged() - .debounce(.milliseconds(1500), scheduler: MainScheduler.instance) - .map { Reactor.Action.searchKeyword($0) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - Observable.zip( - tableView.rx.modelSelected(University.self), - tableView.rx.itemSelected - ) - .delay(.milliseconds(500), scheduler: MainScheduler.instance) - .bind(with: self) { owner, event in - let (item, indexPath) = (event.0, event.1) - owner.tableView.deselectRow(at: indexPath, animated: true) - owner.searchBar.textField.resignFirstResponder() - reactor.action.onNext(.selectUniversity(item)) - } - .disposed(by: disposeBag) - - gradeInputView.didTapUnSelectButton - .bind(with: self) { owner, _ in - owner.setUniversityInput() - } - .disposed(by: disposeBag) - - gradeInputView.didTapSelectGrade - .sink { - reactor.action.onNext(.selectGrade($0+1)) - } - .store(in: &anyCancellable) - - confirmButton.rx.tap - .throttle(.milliseconds(500), scheduler: MainScheduler.instance) - .map { Reactor.Action.confirm } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - notUniversityInfoButton.rx.tap - .throttle(.milliseconds(500), scheduler: MainScheduler.instance) - .map { Reactor.Action.notUnivercityInfo } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } - - private func setGradeInput(to university: University) { - searchContentView.flex.isIncludedInLayout(false).markDirty() - searchContentView.isHidden = true - - gradeInputView.configure(university: university) - gradeInputView.flex.isIncludedInLayout(true).markDirty() - gradeInputView.isHidden = false - view.setNeedsLayout() - } - - private func setUniversityInput() { - searchContentView.flex.isIncludedInLayout(true).markDirty() - searchContentView.isHidden = false - - gradeInputView.flex.isIncludedInLayout(false).markDirty() - gradeInputView.isHidden = true - gradeInputView.selectedIndex = -1 - - view.setNeedsLayout() - } -} - -fileprivate enum Const { - static var title: String { "대학 정보를 알려주세요!" } - static var description: String { "학교 이름과 학년을 선택해주세요." } - static var confirmTitle: String { "가입하기" } - static var university: String { "대학교" } - static var searchBarPlaceholder: String { "ex)머니대학교" } - static var universityInfoEmpty: String { "입력할 대학 정보가 없어요" } -} diff --git a/Tuist/Templates/Module/Module.swift b/Tuist/Templates/Module/Module.swift index 8df3a0bd..1ad59a3e 100644 --- a/Tuist/Templates/Module/Module.swift +++ b/Tuist/Templates/Module/Module.swift @@ -20,9 +20,9 @@ let modulTemplate = Template( path: "Sources/Dummy.swift", contents: "// Dummy" ), - .directory( - path: "Resources", - sourcePath: "Assets.xcassets" + .string( + path: "Testing/Dummy.swift", + contents: "// Dummy" ), .file( path: "Demo/Sources/AppDelegate.swift", @@ -32,10 +32,6 @@ let modulTemplate = Template( path: "Demo/Sources/SceneDelegate.swift", templatePath: "SceneDelegate.stencil" ), - .file( - path: "Demo/Sources/\(moduleNameAttribute)ViewController.swift", - templatePath: "ViewController.stencil" - ), .file( path: "Demo/Resources/LaunchScreen.storyboard", templatePath: "LaunchScreen.stencil" diff --git a/Tuist/Templates/Module/Project.stencil b/Tuist/Templates/Module/Project.stencil index 5538fecd..0b1a6866 100644 --- a/Tuist/Templates/Module/Project.stencil +++ b/Tuist/Templates/Module/Project.stencil @@ -2,6 +2,10 @@ import ProjectDescription let project = Project( name: "{{ name }}", + settings: .settings( + base: .init() + .swiftVersion("5.7") + ), targets: [ Target( name: "{{ name }}", @@ -32,7 +36,19 @@ let project = Project( deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), sources: ["Tests/**"], dependencies: [ - .target(name: "{{ name }}") + .target(name: "{{ name }}"), + .target(name: "{{ name }}Testing") + ] + ), + Target( + name: "{{ name }}Testing", + platform: .iOS, + product: .staticLibrary, + bundleId: "com.framework.moneymong.{{ name }}Testing", + deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), + sources: ["Testing/**"], + dependencies: [ + .target(name: "{{ name }}Interface") ] ), Target( @@ -42,14 +58,29 @@ let project = Project( bundleId: "com.framework.moneymong.{{ name }}Demo", deploymentTarget: .iOS(targetVersion: "15.0", devices: .iphone), infoPlist: .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0", - "CFBundleVersion": "1", - "UILaunchStoryboardName": "LaunchScreen" + "UIUserInterfaceStyle": "Light", + "CFBundleShortVersionString": "1.0", + "CFBundleVersion": "1", + "UILaunchStoryboardName": "LaunchScreen", + "UIApplicationSceneManifest" : [ + "UIApplicationSupportsMultipleScenes":true, + "UISceneConfigurations":[ + "UIWindowSceneSessionRoleApplication":[ + [ + "UISceneConfigurationName":"Default Configuration", + "UISceneDelegateClassName":"$(PRODUCT_MODULE_NAME).SceneDelegate" + ] + ] + ] + ], + "NSLocalNetworkUsageDescription": "Network usage required for debugging purposes", + "NSBonjourServices": ["_pulse._tcp"] ]), sources: ["Demo/Sources/**"], - resources: ["Demo/Resources/**"], + resources: ["Demo/Resources/**"] dependencies: [ - .target(name: "{{ name }}") + .target(name: "{{ name }}"), + .target(name: "{{ name }}Testing") ] ) ] diff --git a/graph.png b/graph.png deleted file mode 100644 index 5f478707..00000000 Binary files a/graph.png and /dev/null differ