diff --git a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/BaseFeatureDependency.swift b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/BaseFeatureDependency.swift new file mode 100644 index 00000000..0bec29a2 --- /dev/null +++ b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/BaseFeatureDependency.swift @@ -0,0 +1,13 @@ +// +// BaseFeatureDependency.swift +// DependencyPlugin +// +// Created by 최준영 on 6/21/24. +// + +import ProjectDescription + +public extension ModuleDependency.Presentation { + + static let BaseFeature: TargetDependency = .project(target: "BaseFeature", path: .relativeToRoot("Projects/Presentation/Feature/Base")) +} diff --git a/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift b/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift index 6feccd93..28803059 100644 --- a/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift +++ b/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift @@ -22,5 +22,10 @@ public struct DataAssembly: Assembly { container.register(AuthRepository.self) { _ in return DefaultAuthRepository() } + + // MARK: 유저프로필 레포지토리 + container.register(UserProfileRepository.self) { _ in + return DefaultUserProfileRepository() + } } } diff --git a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift index a27d820e..a3512f80 100644 --- a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift +++ b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift @@ -24,5 +24,11 @@ public struct DomainAssembly: Assembly { return DefaultAuthUseCase(repository: repository) } + + container.register(CenterProfileUseCase.self) { resolver in + let repository = resolver.resolve(UserProfileRepository.self)! + + return DefaultCenterProfileUseCase(repository: repository) + } } } diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/CenterMainCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift similarity index 83% rename from project/Projects/App/Sources/RootCoordinator/Main/Worker/CenterMainCoordinator.swift rename to project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift index b160eddb..7c0db340 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/CenterMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift @@ -10,7 +10,7 @@ import DSKit import PresentationCore import RootFeature -class CenterMainCoordinator: ParentCoordinator { +class CenterMainCoordinator: CenterMainCoordinatable { var childCoordinators: [Coordinator] = [] var parent: ParentCoordinator? @@ -63,8 +63,10 @@ class CenterMainCoordinator: ParentCoordinator { switch tab { case .recruitmentManage: coordinator = RecruitmentManagementCoordinator( + parent: self, navigationController: navigationController ) + case .setting: coordinator = SettingCoordinator( navigationController: navigationController @@ -75,6 +77,7 @@ class CenterMainCoordinator: ParentCoordinator { // 코디네이터들을 실행 coordinator.start() } + } // MARK: Center 탭의 구성요소들 @@ -91,3 +94,17 @@ enum CenterMainScreen: Int, CaseIterable { } } } + +extension CenterMainCoordinator { + + /// 센터 정보등록 창을 표시합니다. + func centerProfileRegister() { + + let coordinator = CenterProfileRegisterCoordinator(dependency: .init( + navigationController: navigationController, + injector: injector) + ) + addChildCoordinator(coordinator) + coordinator.start() + } +} diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterProfileRegisterCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterProfileRegisterCoordinator.swift new file mode 100644 index 00000000..3729246a --- /dev/null +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterProfileRegisterCoordinator.swift @@ -0,0 +1,70 @@ +// +// CenterProfileRegisterCoordinator.swift +// Idle-iOS +// +// Created by choijunios on 7/27/24. +// + +import UIKit +import CenterFeature +import Entity +import PresentationCore +import UseCaseInterface + +class CenterProfileRegisterCoordinator: CenterProfileRegisterCoordinatable { + + var childCoordinators: [Coordinator] = [] + + var parent: ParentCoordinator? + + var navigationController: UINavigationController + let injector: Injector + + init(dependency: Dependency) { + self.navigationController = dependency.navigationController + self.injector = dependency.injector + } + + func start() { + + let coordinator = RegisterCenterInfoCoordinator( + profileUseCase: injector.resolve(CenterProfileUseCase.self), + navigationController: navigationController + ) + + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } + + public func registerFinished() { + + clearChildren() + + parent?.removeChildCoordinator(self) + } +} + +extension CenterProfileRegisterCoordinator { + + func showCompleteScreen(cardVO: Entity.CenterProfileCardVO) { + let coordinator = ProfileRegisterCompleteCoordinator( + cardVO: cardVO, + navigationController: navigationController + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } + + func showMyCenterProfile() { + let coordinator = CenterProfileCoordinator( + mode: .myProfile, + profileUseCase: injector.resolve(CenterProfileUseCase.self), + navigationController: navigationController + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } +} diff --git a/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift b/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift index 5d6204bf..a5b5dcc7 100644 --- a/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift +++ b/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift @@ -10,6 +10,7 @@ import AuthFeature extension RootCoordinator { + /// 로그인및 회원가입을 실행합니다. func auth() { let authCoordinator = AuthCoordinator( @@ -53,4 +54,5 @@ extension RootCoordinator { coordinator.start() } + } diff --git a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift index 067f775a..a91ee8c2 100644 --- a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift +++ b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift @@ -60,9 +60,7 @@ public extension DefaultAuthRepository { genderType: registerState.gender, phoneNumber: registerState.phoneNumber, roadNameAddress: registerState.addressInformation.roadAddress, - lotNumberAddress: registerState.addressInformation.jibunAddress, - longitude: registerState.latitude, - latitude: registerState.logitude + lotNumberAddress: registerState.addressInformation.jibunAddress ) let data = (try? JSONEncoder().encode(dto)) ?? Data() diff --git a/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift index 4cad3a36..c26d3c2d 100644 --- a/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift +++ b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserProfileRepository.swift @@ -27,10 +27,43 @@ public class DefaultUserProfileRepository: UserProfileRepository { } } - public func getCenterProfile() -> Single { + /// 센터프로필(최초 센터정보)를 등록합니다. + public func registerCenterProfileForText(state: CenterProfileRegisterState) -> Single { + let dto = RegisterCenterProfileDTO( + centerName: state.centerName, + officeNumber: state.officeNumber, + roadNameAddress: state.roadNameAddress, + lotNumberAddress: state.lotNumberAddress, + detailedAddress: state.detailedAddress, + introduce: state.introduce + ) + let data = try! JSONEncoder().encode(dto) + + return userInformationService + .request(api: .registerCenterProfile(data: data), with: .withToken) + .map { _ in () } + } + + public func getCenterProfile(mode: ProfileMode) -> Single { + + var api: UserInformationAPI! + + switch mode { + case .myProfile: + api = .getCenterProfile + case .otherProfile(let id): + api = .getOtherCenterProfile(id: id) + } + + return userInformationService + .requestDecodable(api: api, with: .withToken) + .map { (dto: CenterProfileDTO) in dto.toEntity() } + } + + public func getCenterProfile(id: String) -> Single { userInformationService - .requestDecodable(api: .getCenterProfile, with: .withToken) + .requestDecodable(api: .getOtherCenterProfile(id: id), with: .withToken) .map { (dto: CenterProfileDTO) in dto.toEntity() } } @@ -45,7 +78,6 @@ public class DefaultUserProfileRepository: UserProfileRepository { /// 이미지 업로드 public func uploadImage(_ userType: UserType, imageInfo: ImageUploadInfo) -> Single { - getPreSignedUrl(userType, ext: imageInfo.ext) .flatMap { [unowned self] dto in self.uploadImageToPreSignedUrl(url: dto.uploadUrl, data: imageInfo.data) diff --git a/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift index c3005e5f..225b77cd 100644 --- a/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift @@ -23,8 +23,12 @@ extension UserType { public enum UserInformationAPI { + // 프로필 생성 + case registerCenterProfile(data: Data) + // 프로필 조회 case getCenterProfile + case getOtherCenterProfile(id: String) case updateCenterProfile(officeNumber: String, introduce: String?) // 프로필 사진 업로드 @@ -43,8 +47,12 @@ extension UserInformationAPI: BaseAPI { public var path: String { switch self { + case .registerCenterProfile: + "center/my/profile" case .getCenterProfile: "center/my/profile" + case .getOtherCenterProfile(let id): + "center/profile/\(id)" case .updateCenterProfile: "center/my/profile" case .getPreSignedUrl(let type, _): @@ -56,8 +64,12 @@ extension UserInformationAPI: BaseAPI { public var method: Moya.Method { switch self { + case .registerCenterProfile: + .post case .getCenterProfile: .get + case .getOtherCenterProfile: + .get case .updateCenterProfile: .patch case .getPreSignedUrl: @@ -76,8 +88,8 @@ extension UserInformationAPI: BaseAPI { public var task: Moya.Task { switch self { - case .getCenterProfile: - return .requestPlain + case .registerCenterProfile(let data): + return .requestData(data) case .updateCenterProfile(let officeNumber, let introduce): var bodyData: [String: String] = ["officeNumber": officeNumber] if let introduce { @@ -95,7 +107,8 @@ extension UserInformationAPI: BaseAPI { "imageFileExtension": imageExt ] return .requestParameters(parameters: params, encoding: parameterEncoding) + default: + return .requestPlain } } - } diff --git a/project/Projects/Data/NetworkDataSource/DTO/Auth/WorkerRegistrationDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/Auth/WorkerRegistrationDTO.swift index 3ffa19df..2fa123f5 100644 --- a/project/Projects/Data/NetworkDataSource/DTO/Auth/WorkerRegistrationDTO.swift +++ b/project/Projects/Data/NetworkDataSource/DTO/Auth/WorkerRegistrationDTO.swift @@ -15,18 +15,14 @@ public struct WorkerRegistrationDTO: Encodable { public let phoneNumber: String public let roadNameAddress: String public let lotNumberAddress: String - public let longitude: String - public let latitude: String - public init(carerName: String, birthYear: Int, genderType: Gender, phoneNumber: String, roadNameAddress: String, lotNumberAddress: String, longitude: String, latitude: String) { + public init(carerName: String, birthYear: Int, genderType: Gender, phoneNumber: String, roadNameAddress: String, lotNumberAddress: String) { self.carerName = carerName self.birthYear = birthYear self.genderType = Self.convertGenderValue(genderType) self.phoneNumber = phoneNumber self.roadNameAddress = roadNameAddress self.lotNumberAddress = lotNumberAddress - self.longitude = longitude - self.latitude = latitude } } diff --git a/project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift index 331f35b5..e1abc48b 100644 --- a/project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift +++ b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/CenterProfileDTO.swift @@ -9,14 +9,14 @@ import Foundation import Entity public struct CenterProfileDTO: Codable { - let centerName: String? - let officeNumber: String? - let roadNameAddress: String? - let lotNumberAddress: String? - let detailedAddress: String? + let centerName: String + let officeNumber: String + let roadNameAddress: String + let lotNumberAddress: String + let detailedAddress: String + let introduce: String? let longitude: String? let latitude: String? - let introduce: String? let profileImageUrl: String? } @@ -24,11 +24,11 @@ public extension CenterProfileDTO { func toEntity() -> CenterProfileVO { CenterProfileVO( - centerName: centerName ?? "", - officeNumber: officeNumber ?? "", - roadNameAddress: roadNameAddress ?? "", - lotNumberAddress: lotNumberAddress ?? "", - detailedAddress: detailedAddress ?? "", + centerName: centerName, + officeNumber: officeNumber, + roadNameAddress: roadNameAddress, + lotNumberAddress: lotNumberAddress, + detailedAddress: detailedAddress, longitude: longitude ?? "", latitude: latitude ?? "", introduce: introduce ?? "", diff --git a/project/Projects/Data/NetworkDataSource/DTO/UserInfo/RegisterCenterProfileDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/RegisterCenterProfileDTO.swift new file mode 100644 index 00000000..c586a04c --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/DTO/UserInfo/RegisterCenterProfileDTO.swift @@ -0,0 +1,27 @@ +// +// RegisterCenterProfileDTO.swift +// NetworkDataSource +// +// Created by choijunios on 7/27/24. +// + +import Foundation +import Entity + +public struct RegisterCenterProfileDTO: Codable { + public let centerName: String + public let officeNumber: String + public let roadNameAddress: String + public let lotNumberAddress: String + public let detailedAddress: String + public let introduce: String + + public init(centerName: String, officeNumber: String, roadNameAddress: String, lotNumberAddress: String, detailedAddress: String, introduce: String) { + self.centerName = centerName + self.officeNumber = officeNumber + self.roadNameAddress = roadNameAddress + self.lotNumberAddress = lotNumberAddress + self.detailedAddress = detailedAddress + self.introduce = introduce + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift index 632847ed..5f243ccf 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift @@ -19,11 +19,10 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { self.repository = repository } - public func getProfile() -> Single> { - convert(task: repository - .getCenterProfile()) { [unowned self] error in - toDomainError(error: error) - } + public func getProfile(mode: ProfileMode) -> Single> { + convert(task: repository.getCenterProfile(mode: mode)) { [unowned self] error in + toDomainError(error: error) + } } public func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> { @@ -63,7 +62,7 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { return .error(error) } - let updateImageResult = updateImage + let uploadImageResult = updateImage .catch { error in if let httpExp = error as? HTTPResponseException { let newError = HTTPResponseException( @@ -80,7 +79,63 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { let task = Observable .zip( updateTextResult.asObservable(), - updateImageResult.asObservable() + uploadImageResult.asObservable() + ) + .map { _ in () } + .asSingle() + + return convert(task: task) { [unowned self] error in + toDomainError(error: error) + } + } + + public func registerCenterProfile(state: CenterProfileRegisterState) -> Single> { + + var registerImage: Single! + + let imageInfo = state.imageInfo + + if let imageInfo { + registerImage = repository.uploadImage( + .center, + imageInfo: imageInfo + ) + } else { + registerImage = .just(()) + } + + let registerTextResult = repository.registerCenterProfileForText(state: state) + .catch { error in + if let httpExp = error as? HTTPResponseException { + let newError = HTTPResponseException( + status: httpExp.status, + rawCode: "Err-001", + timeStamp: httpExp.timeStamp + ) + + return .error(newError) + } + return .error(error) + } + + let uploadImageResult = registerImage + .catch { error in + if let httpExp = error as? HTTPResponseException { + let newError = HTTPResponseException( + status: httpExp.status, + rawCode: "Err-002", + timeStamp: httpExp.timeStamp + ) + + return .error(newError) + } + return .error(error) + } + + let task = Observable + .zip( + registerTextResult.asObservable(), + uploadImageResult.asObservable() ) .map { _ in () } .asSingle() diff --git a/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift b/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift index 3fc0068c..8c3a06ea 100644 --- a/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift +++ b/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift @@ -13,8 +13,6 @@ public class WorkerRegisterState { public var birthYear: Int = 0 public var phoneNumber: String = "" public var addressInformation: AddressInformation = .init(roadAddress: "", jibunAddress: "") - public var latitude: String = "" - public var logitude: String = "" public init() { } @@ -25,8 +23,6 @@ public class WorkerRegisterState { phoneNumber = "" addressInformation.roadAddress = "" addressInformation.jibunAddress = "" - latitude = "" - logitude = "" } } diff --git a/project/Projects/Domain/Entity/State/Profile/CenterInfoRegisterState.swift b/project/Projects/Domain/Entity/State/Profile/CenterInfoRegisterState.swift new file mode 100644 index 00000000..39bba33d --- /dev/null +++ b/project/Projects/Domain/Entity/State/Profile/CenterInfoRegisterState.swift @@ -0,0 +1,24 @@ +// +// CenterProfileRegisterState.swift +// Entity +// +// Created by choijunios on 7/27/24. +// + +import Foundation + +public class CenterProfileRegisterState { + + // 모든 값들 required + public var centerName: String = "" + public var officeNumber: String = "" + public var roadNameAddress: String = "" + public var lotNumberAddress: String = "" + public var detailedAddress: String = "" + public var introduce: String = "" + + // 추후 이미지를 옵셔널로 바꿀수도 있음 + public var imageInfo: ImageUploadInfo? = nil + + public init() { } +} diff --git a/project/Projects/Domain/Entity/State/Profile/ProfileMode.swift b/project/Projects/Domain/Entity/State/Profile/ProfileMode.swift new file mode 100644 index 00000000..89dbf706 --- /dev/null +++ b/project/Projects/Domain/Entity/State/Profile/ProfileMode.swift @@ -0,0 +1,13 @@ +// +// ProfileMode.swift +// Entity +// +// Created by choijunios on 7/29/24. +// + +import Foundation + +public enum ProfileMode { + case myProfile + case otherProfile(id: String) +} diff --git a/project/Projects/Domain/Entity/State/User/Gender.swift b/project/Projects/Domain/Entity/State/Util/Gender.swift similarity index 100% rename from project/Projects/Domain/Entity/State/User/Gender.swift rename to project/Projects/Domain/Entity/State/Util/Gender.swift diff --git a/project/Projects/Domain/Entity/State/User/UserType.swift b/project/Projects/Domain/Entity/State/Util/UserType.swift similarity index 100% rename from project/Projects/Domain/Entity/State/User/UserType.swift rename to project/Projects/Domain/Entity/State/Util/UserType.swift diff --git a/project/Projects/Domain/Entity/VO/UserInfo/CenterProfileCardVO.swift b/project/Projects/Domain/Entity/VO/UserInfo/CenterProfileCardVO.swift new file mode 100644 index 00000000..db189953 --- /dev/null +++ b/project/Projects/Domain/Entity/VO/UserInfo/CenterProfileCardVO.swift @@ -0,0 +1,20 @@ +// +// CenterProfileCardVO.swift +// Entity +// +// Created by choijunios on 7/29/24. +// + +import Foundation + +public struct CenterProfileCardVO { + public let name: String + public let location: String + + public init(name: String, location: String) { + self.name = name + self.location = location + } + + public static let `default` = CenterProfileCardVO(name: "내 센터", location: "내 센터 위치") +} diff --git a/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift b/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift index cbc6ca84..20a5c8b5 100644 --- a/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/UserInfo/UserProfileRepository.swift @@ -11,7 +11,9 @@ import Entity public protocol UserProfileRepository: RepositoryBase { - func getCenterProfile() -> Single + func registerCenterProfileForText(state: CenterProfileRegisterState) -> Single + + func getCenterProfile(mode: ProfileMode) -> Single func updateCenterProfileForText(phoneNumber: String, introduction: String?) -> Single // ImageUpload diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift index 7809cfc0..8052247d 100644 --- a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift @@ -9,13 +9,23 @@ import Foundation import RxSwift import Entity -/// 1. 센터 프로필 정보 조회 +/// 1. 나의 센터 프로필 정보 조회 /// 2. 센터 프로필 정보 업데이트(전화번호, 센터소개글) /// 3. 센터 프로필 정보 업데이트(이미지, pre-signed-url) /// 4. 센터 프로필 정보 업데이트(이미지, pre-signed-url-callback) +/// 5. 센터 프로필 최초 등록 +/// 6. 특정 센터의 프로필 불러오기 public protocol CenterProfileUseCase: UseCaseBase { - func getProfile() -> Single> + /// 1. 나의 센터/다른 센터 프로필 정보 조회 + func getProfile(mode: ProfileMode) -> Single> + + /// 2. 센터 프로필 정보 업데이트(전화번호, 센터소개글) + /// 3. 센터 프로필 정보 업데이트(이미지, pre-signed-url) + /// 4. 센터 프로필 정보 업데이트(이미지, pre-signed-url-callback) func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> + + /// 5. 센터 프로필 최초 등록 + func registerCenterProfile(state: CenterProfileRegisterState) -> Single> } diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift index 9bdeb8f8..ae27d3ce 100644 --- a/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift @@ -17,37 +17,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = scene as? UIWindowScene else { return } window = UIWindow(windowScene: windowScene) - let vc = IdleTabBar() - vc.setViewControllers(info: [ - TabBarInfo( - viewController: UINavigationController(rootViewController: { - let vc = UIViewController() - vc.view.backgroundColor = .blue - return vc - }()), - tabBarItem: .init(name: "홈") - ), - - TabBarInfo( - viewController: UINavigationController(rootViewController: { - let vc = UIViewController() - vc.view.backgroundColor = .yellow - return vc - }()), - tabBarItem: .init(name: "프로필") - ), - - TabBarInfo( - viewController: UINavigationController(rootViewController: { - let vc = UIViewController() - vc.view.backgroundColor = .red - return vc - }()), - tabBarItem: .init(name: "설정") - ), - ]) + - vc.selectedIndex = 0 + let vc = ViewController4() window?.rootViewController = vc window?.makeKeyAndVisible() diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController4.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController4.swift new file mode 100644 index 00000000..fb5a2129 --- /dev/null +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController4.swift @@ -0,0 +1,49 @@ +// +// ViewController4.swift +// DSKitExampleApp +// +// Created by choijunios on 7/27/24. +// + +import UIKit +import DSKit + +class ViewController4: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + view.backgroundColor = .white + + let btn = CenterProfileButton( + nameString: "세얼간이 요양보호소", + locatonString: "용인시 어쩌고 저쩌고", + isArrow: true + ) + +// btn.isArrow.accept(false) + + view.addSubview(btn) + btn.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + btn.centerYAnchor.constraint(equalTo: view.centerYAnchor), + btn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + btn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + ]) + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/camera.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/camera.imageset/Contents.json new file mode 100644 index 00000000..82cb0eeb --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/camera.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "camera.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/camera.imageset/camera.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/camera.imageset/camera.svg new file mode 100644 index 00000000..bb9e9b48 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/camera.imageset/camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/chevronRight.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/chevronRight.imageset/Contents.json new file mode 100644 index 00000000..f464bae1 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/chevronRight.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "chevronRight.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/chevronRight.imageset/chevronRight.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/chevronRight.imageset/chevronRight.svg new file mode 100644 index 00000000..37352c54 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/chevronRight.imageset/chevronRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/picture.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/picture.imageset/Contents.json new file mode 100644 index 00000000..ea59fab9 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/picture.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "picture.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/picture.imageset/picture.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/picture.imageset/picture.svg new file mode 100644 index 00000000..1b6c41e2 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/picture.imageset/picture.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/ImageView/ImageSelectView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/ImageView/ImageSelectView.swift new file mode 100644 index 00000000..d873f29a --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/ImageView/ImageSelectView.swift @@ -0,0 +1,195 @@ +// +// ImageSelectView.swift +// DSKit +// +// Created by choijunios on 7/26/24. +// + +import UIKit +import RxSwift +import RxCocoa +import Entity + +public class ImageSelectView: UIImageView { + + public enum State { + case editing + case normal + } + + // Init + public private(set) var state: BehaviorRelay + + public weak var viewController: UIViewController! + + // Optinal values + public var onError: (()->())? + + // image + public private(set) var displayingImage: BehaviorRelay = .init(value: nil) + public private(set) var selectedImage: BehaviorRelay = .init(value: nil) + + public init(state: State, viewController: UIViewController) { + self.state = .init(value: state) + self.viewController = viewController + super.init(frame: .zero) + + setAppearacne() + setLayout() + setObservable() + } + + public required init?(coder: NSCoder) { fatalError() } + + // View + /// PlaceHolderView(Edit) + let placeholderViewForEdit: TappableUIView = { + let view = TappableUIView() + view.backgroundColor = DSKitAsset.Colors.gray100.color + let imageView = DSKitAsset.Icons.camera.image.toView() + view.addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints=false + imageView.tintColor = .white + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 54), + imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor), + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + return view + }() + + /// PlaceHolderView(Normal) + let placeholderViewForNormal: UIView = { + let view = UIView() + view.backgroundColor = DSKitAsset.Colors.gray050.color + let imageView = DSKitAsset.Icons.picture.image.toView() + view.addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints=false + imageView.tintColor = DSKitAsset.Colors.gray100.color + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 38), + imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor), + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + return view + }() + + /// Edit ButtonView + let centerImageEditButton: UIButton = { + let btn = UIButton() + btn.setImage(DSKitAsset.Icons.editPhoto.image, for: .normal) + btn.isUserInteractionEnabled = true + return btn + }() + + private let disposeBag = DisposeBag() + + private func setAppearacne() { + + self.isUserInteractionEnabled = true + + self.layer.cornerRadius = 6 + self.clipsToBounds = true + self.contentMode = .scaleToFill + } + + private func setLayout() { + + [ + placeholderViewForEdit, + placeholderViewForNormal, + centerImageEditButton + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + placeholderViewForEdit.topAnchor.constraint(equalTo: self.topAnchor), + placeholderViewForEdit.leadingAnchor.constraint(equalTo: self.leadingAnchor), + placeholderViewForEdit.trailingAnchor.constraint(equalTo: self.trailingAnchor), + placeholderViewForEdit.bottomAnchor.constraint(equalTo: self.bottomAnchor), + + placeholderViewForNormal.topAnchor.constraint(equalTo: self.topAnchor), + placeholderViewForNormal.leadingAnchor.constraint(equalTo: self.leadingAnchor), + placeholderViewForNormal.trailingAnchor.constraint(equalTo: self.trailingAnchor), + placeholderViewForNormal.bottomAnchor.constraint(equalTo: self.bottomAnchor), + + centerImageEditButton.widthAnchor.constraint(equalToConstant: 28), + centerImageEditButton.heightAnchor.constraint(equalTo: centerImageEditButton.widthAnchor), + + centerImageEditButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), + centerImageEditButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16), + ]) + + } + + private func setObservable() { + + Observable + .combineLatest( + displayingImage, + state + ) + .subscribe(onNext: { [weak self] (image, state) in + + guard let self else { return } + + if image != nil { + self.image = image + placeholderViewForEdit.isHidden = true + placeholderViewForNormal.isHidden = true + centerImageEditButton.isHidden = state == .normal + } else { + placeholderViewForEdit.isHidden = state == .normal + placeholderViewForNormal.isHidden = state == .editing + centerImageEditButton.isHidden = true + } + }) + .disposed(by: disposeBag) + + Observable + .merge( + placeholderViewForEdit.rx.tap.asObservable(), + centerImageEditButton.rx.tap.asObservable() + ) + .asDriver(onErrorJustReturn: ()) + .drive(onNext: { [unowned self] in + showPhotoGalley() + }) + .disposed(by: disposeBag) + } +} + +extension ImageSelectView: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + private func showPhotoGalley() { + + let imagePickerVC = UIImagePickerController() + imagePickerVC.delegate = self + + if !UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { + onError?() + return + } + + imagePickerVC.sourceType = .photoLibrary + viewController.present(imagePickerVC, animated: true) + } + + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + + // image + selectedImage.accept(image) + + picker.dismiss(animated: true) + } + } +} + + + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/ProfileButton/CenterProfileButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/ProfileButton/CenterProfileButton.swift new file mode 100644 index 00000000..9578dbd7 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/ProfileButton/CenterProfileButton.swift @@ -0,0 +1,140 @@ +// +// CenterProfileButton.swift +// DSKit +// +// Created by choijunios on 7/27/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class CenterProfileButton: TappableUIView { + + // Init + public let nameString: String + public let locatonString: String + public let isArrow: BehaviorRelay + + // View + public private(set) lazy var nameLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle3) + label.textString = nameString + label.textAlignment = .left + return label + }() + + public private(set) lazy var addressLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.textString = locatonString + label.attrTextColor = DSKitAsset.Colors.gray500.color + label.textAlignment = .left + return label + }() + + private let chevronRightImage: UIImageView = { + let image = DSKitAsset.Icons.chevronRight.image.toView() + image.tintColor = DSKitAsset.Colors.gray200.color + return image + }() + + private let disposeBag = DisposeBag() + + public init(nameString: String, locatonString: String, isArrow: Bool = false) { + self.nameString = nameString + self.locatonString = locatonString + self.isArrow = .init(value: isArrow) + + super.init() + + setApearance() + setLayout() + setObservable() + } + required init?(coder: NSCoder) { fatalError() } + + private func setApearance() { + + self.backgroundColor = .white + self.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor + self.layer.borderWidth = 1 + self.layer.cornerRadius = 8 + + self.layoutMargins = .init(top: 16, left: 20, bottom: 16, right: 20) + } + + private func setLayout() { + + let locImage = DSKitAsset.Icons.locationSmall.image.toView() + let locationStack = HStack( + [ + locImage, + addressLabel, + ], + spacing: 2, + distribution: .fill + ) + locImage.translatesAutoresizingMaskIntoConstraints = false + addressLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + locImage.widthAnchor.constraint(equalToConstant: 20), + locImage.heightAnchor.constraint(equalTo: locImage.widthAnchor), + + addressLabel.heightAnchor.constraint(equalTo: locImage.heightAnchor) + ]) + + let textStack = VStack( + [ + nameLabel, + locationStack + ], + spacing: 4, + alignment: .leading + ) + + let mainStack = HStack( + [ + textStack, + chevronRightImage + ], + spacing: 14, + alignment: .center + ) + NSLayoutConstraint.activate([ + chevronRightImage.widthAnchor.constraint(equalToConstant: 24), + chevronRightImage.heightAnchor.constraint(equalTo: locImage.widthAnchor), + ]) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + mainStack.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + } + + private func setObservable() { + super.rx.tap + .subscribe { [weak self] _ in + + self?.alpha = 0.5 + + UIView.animate(withDuration: 0.2) { + self?.alpha = 1 + } + } + .disposed(by: disposeBag) + + isArrow + .map { !$0 } + .bind(to: chevronRightImage.rx.isHidden) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift index e5b756d3..b9275cc0 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift @@ -8,8 +8,12 @@ import UIKit import RxSwift import RxCocoa +import PresentationCore public class IdleTabBar: UIViewController { + + // Coordinator + public weak var coordinator: ParentCoordinator? // 탭바구성 public private(set) var viewControllers: [UIViewController] = [] diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Base/TappableUIView.swift b/project/Projects/Presentation/DSKit/Sources/Component/Base/TappableUIView.swift new file mode 100644 index 00000000..3f4668e5 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/Component/Base/TappableUIView.swift @@ -0,0 +1,30 @@ +// +// TappableUIView.swift +// DSKit +// +// Created by choijunios on 7/27/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class TappableUIView: UIView { + + public init() { + super.init(frame: .zero) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTouchAction)) + self.addGestureRecognizer(tapGesture) + } + required init?(coder: NSCoder) { fatalError() } + + @objc + func onTouchAction(_ tapGesture: UITapGestureRecognizer) { } +} + +public extension Reactive where Base: TappableUIView { + var tap: ControlEvent { + let source = self.methodInvoked(#selector(Base.onTouchAction)).map { _ in } + return ControlEvent(events: source) + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift b/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift index 53ec7ca2..ff1d4047 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift @@ -75,13 +75,15 @@ public class IdleTextField: UITextField { private let disposeBag = DisposeBag() + /// 타이포그래피를 변경하면, 앞전에 설정한 속성값을 덥 public var typography: Typography { get { currentTypography } set { currentTypography = newValue - self.setNeedsLayout() + defaultTextAttributes = currentTypography.attributes + self.updateText() } } @@ -102,14 +104,14 @@ public class IdleTextField: UITextField { attributedPlaceholder?.string ?? "" } set { - attributedPlaceholder = typography.attributes.toString( + attributedPlaceholder = currentTypography.attributes.toString( newValue, with: DSKitAsset.Colors.gray200.color ) } } private func updateText() { - self.rx.attributedText.onNext(NSAttributedString(string: textString, attributes: typography.attributes)) + self.rx.attributedText.onNext(NSAttributedString(string: textString, attributes: currentTypography.attributes)) } } diff --git a/project/Projects/Presentation/Feature/Auth/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Auth/ExampleApp/Sources/SceneDelegate.swift index 703cec7e..0b9fca5d 100644 --- a/project/Projects/Presentation/Feature/Auth/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Auth/ExampleApp/Sources/SceneDelegate.swift @@ -21,13 +21,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) - window?.rootViewController = CenterLoginViewController( - viewModel: CenterLoginViewModel( - authUseCase: DefaultAuthUseCase( - repository: DefaultAuthRepository() - ) - ) - ) + window?.rootViewController = UIViewController() window?.makeKeyAndVisible() } } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift index 8368333f..de318edb 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift @@ -11,6 +11,7 @@ import DSKit import RxSwift import RxCocoa import PresentationCore +import BaseFeature public class CenterAuthMainViewController: DisposableViewController { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift index 7045d86d..c111e4a6 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift @@ -9,6 +9,7 @@ import UIKit import RxSwift import DSKit import PresentationCore +import BaseFeature public class CenterLoginViewController: BaseViewController { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterSetNewPasswordController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterSetNewPasswordController.swift index 658fa10b..874f4f2d 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterSetNewPasswordController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterSetNewPasswordController.swift @@ -10,6 +10,7 @@ import DSKit import RxSwift import RxCocoa import PresentationCore +import BaseFeature class CenterSetNewPasswordController: DisposableViewController { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift index a2367ed8..6b02b355 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift @@ -10,6 +10,7 @@ import DSKit import RxCocoa import RxSwift import PresentationCore +import BaseFeature public protocol ChangePasswordSuccessInputable { var changePasswordButtonClicked: PublishRelay { get } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift index fea73ad9..125ab2fb 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift @@ -11,6 +11,7 @@ import DSKit import RxSwift import RxCocoa import PresentationCore +import BaseFeature public protocol AuthBusinessOwnerInputable { var editingBusinessNumber: BehaviorRelay { get set } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/CenterRegisterViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/CenterRegisterViewController.swift index be5de7fd..b1588f29 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/CenterRegisterViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/CenterRegisterViewController.swift @@ -10,6 +10,7 @@ import DSKit import RxCocoa import RxSwift import PresentationCore +import BaseFeature class CenterRegisterViewController: DisposableViewController { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift index c9fe9eae..3c4fb349 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift @@ -11,6 +11,7 @@ import DSKit import RxSwift import RxCocoa import PresentationCore +import BaseFeature public protocol SetIdInputable { var editingId: BehaviorRelay { get set } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift index 75da09e3..306c3323 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift @@ -10,6 +10,7 @@ import DSKit import RxSwift import RxCocoa import PresentationCore +import BaseFeature public protocol EnterNameInputable { var editingName: PublishRelay { get } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift index 6704de6b..f199b1ed 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift @@ -11,6 +11,7 @@ import RxCocoa import DSKit import Entity import PresentationCore +import BaseFeature public protocol AuthPhoneNumberInputable { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/SelectAuthTypeViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/SelectAuthTypeViewController.swift index a671773c..5dac8e71 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/SelectAuthTypeViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/SelectAuthTypeViewController.swift @@ -10,6 +10,7 @@ import DSKit import RxSwift import RxCocoa import PresentationCore +import BaseFeature class SelectAuthTypeViewController: DisposableViewController { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift index 8003d07c..3c7dcde5 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift @@ -11,6 +11,7 @@ import RxCocoa import DSKit import Entity import PresentationCore +import BaseFeature public protocol EnterAddressInputable { var addressInformation: PublishRelay { get } @@ -209,7 +210,7 @@ where T.Input: EnterAddressInputable & CTAButtonEnableInputable, T.Output: Regis private func showDaumSearchView() { let vc = DaumAddressSearchViewController() - vc.deleage = self + vc.delegate = self vc.modalPresentationStyle = .fullScreen navigationController?.pushViewController(vc, animated: true) } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EntetPersonalInfoViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EntetPersonalInfoViewController.swift index 47616893..83fc43b7 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EntetPersonalInfoViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EntetPersonalInfoViewController.swift @@ -11,6 +11,7 @@ import RxCocoa import DSKit import Entity import PresentationCore +import BaseFeature protocol WorkerPersonalInfoInputable: EnterNameInputable, SelectGenderInputable { var edtingBirthYear: PublishRelay { get } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift index 23cd8c14..1814d400 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift @@ -11,6 +11,7 @@ import RxCocoa import DSKit import Entity import PresentationCore +import BaseFeature protocol SelectGenderInputable { var selectingGender: BehaviorRelay { get } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerAuthMainViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerAuthMainViewController.swift index bbbb0d57..86a013e5 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerAuthMainViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerAuthMainViewController.swift @@ -11,6 +11,7 @@ import DSKit import RxSwift import RxCocoa import PresentationCore +import BaseFeature public class WorkerAuthMainViewController: DisposableViewController { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerRegisterViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerRegisterViewController.swift index 56ee3dd3..62ce45ff 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerRegisterViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/WorkerRegisterViewController.swift @@ -10,6 +10,7 @@ import DSKit import RxCocoa import RxSwift import PresentationCore +import BaseFeature class WorkerRegisterViewController: DisposableViewController { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift index b582d20c..e58fb4da 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift @@ -12,7 +12,6 @@ import PresentationCore import UseCaseInterface import Entity - extension AuthInOutStreamManager { static func idInOut( diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift index 26cc6c30..73e42a07 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift @@ -6,10 +6,12 @@ // import RxSwift +import BaseFeature import RxCocoa import UseCaseInterface import Entity import PresentationCore +import BaseFeature public class CenterLoginViewModel: ViewModelType { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift index d4f65641..867f6f2e 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift @@ -10,6 +10,7 @@ import RxCocoa import UseCaseInterface import Entity import PresentationCore +import BaseFeature public class CenterSetNewPasswordViewModel: ViewModelType { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift index 8f51ea50..d55c1516 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift @@ -11,6 +11,7 @@ import RxCocoa import PresentationCore import UseCaseInterface import Entity +import BaseFeature public class CenterRegisterViewModel: ViewModelType { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift index 441aab15..a196ea4d 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift @@ -11,6 +11,7 @@ import RxCocoa import PresentationCore import UseCaseInterface import Entity +import BaseFeature public class WorkerRegisterViewModel: ViewModelType { @@ -85,13 +86,9 @@ public class WorkerRegisterViewModel: ViewModelType { // 예외적으로 ViewModel에서 구독처리 input .addressInformation - .subscribe { [unowned self] info in - self.stateObject.addressInformation = info - - // TODO: 위동 경도 API 적용 - self.stateObject.latitude = "37.5036833" - self.stateObject.logitude = "127.0448556" - } + .subscribe(onNext: { [stateObject] info in + stateObject.addressInformation = info + }) .disposed(by: disposeBag) registerInOut() diff --git a/project/Projects/Presentation/Feature/Base/ExampleApp/Resources/LaunchScreen.storyboard b/project/Projects/Presentation/Feature/Base/ExampleApp/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..a2157a3e --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/ExampleApp/Resources/LaunchScreen.storyboard @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/AppDelegate.swift b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/AppDelegate.swift new file mode 100644 index 00000000..00267bb5 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// +// +// Created by 최준영 on 6/19/24. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/SceneDelegate.swift new file mode 100644 index 00000000..015452b5 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/SceneDelegate.swift @@ -0,0 +1,23 @@ +// +// SceneDelegate.swift +// +// +// Created by 최준영 on 6/19/24. +// + +import UIKit + +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) + window?.rootViewController = ViewController() + window?.makeKeyAndVisible() + } +} diff --git a/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/ViewController.swift b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/ViewController.swift new file mode 100644 index 00000000..e439d432 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/ViewController.swift @@ -0,0 +1,29 @@ +// +// ViewController.swift +// +// +// Created by 최준영 on 6/19/24. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + + let initialLabel = UILabel() + + initialLabel.text = "Example app" + + view.backgroundColor = .white + + view.addSubview(initialLabel) + initialLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + initialLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + initialLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } +} + diff --git a/project/Projects/Presentation/Feature/Base/Project.swift b/project/Projects/Presentation/Feature/Base/Project.swift new file mode 100644 index 00000000..f5ac7ba9 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Project.swift @@ -0,0 +1,81 @@ +// +// Project.swift +// ProjectDescriptionHelpers +// +// Created by choijunios on 2024/07/27 +// + +import ProjectDescription +import ProjectDescriptionHelpers +import ConfigurationPlugin +import DependencyPlugin + +let project = Project( + name: "Base", + settings: .settings( + configurations: IdleConfiguration.emptyConfigurations + ), + targets: [ + + /// FeatureConcrete + .target( + name: "BaseFeature", + destinations: DeploymentSettings.platform, + product: .staticFramework, + bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)", + deploymentTargets: DeploymentSettings.deployment_version, + sources: ["Sources/**"], + resources: ["Resources/**"], + dependencies: [ + // Presentation + D.Presentation.PresentationCore, + D.Presentation.DSKit, + + // Domain + D.Domain.UseCaseInterface, + D.Domain.RepositoryInterface, + + // ThirdParty + D.ThirdParty.RxSwift, + D.ThirdParty.RxCocoa, + ], + settings: .settings( + configurations: IdleConfiguration.presentationConfigurations + ) + ), + + /// FeatureConcrete ExampleApp + .target( + name: "Base_ExampleApp", + destinations: DeploymentSettings.platform, + product: .app, + bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)", + deploymentTargets: DeploymentSettings.deployment_version, + infoPlist: IdleInfoPlist.exampleAppDefault, + sources: ["ExampleApp/Sources/**"], + resources: ["ExampleApp/Resources/**"], + dependencies: [ + .target(name: "BaseFeature"), + ], + settings: .settings( + configurations: IdleConfiguration.presentationConfigurations + ) + ), + ], + schemes: [ + Scheme.makeSchemes( + .target("BaseFeature"), + configNames: [ + IdleConfiguration.debugConfigName, + IdleConfiguration.releaseConfigName + ] + ), + Scheme.makeSchemes( + .target("Base_ExampleApp"), + configNames: [ + IdleConfiguration.debugConfigName, + IdleConfiguration.releaseConfigName + ] + ) + ].flatMap { $0 } +) diff --git a/project/Projects/Presentation/Feature/Base/Resources/Empty.md b/project/Projects/Presentation/Feature/Base/Resources/Empty.md new file mode 100644 index 00000000..64e53d46 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Resources/Empty.md @@ -0,0 +1,2 @@ +# <#Title#> + diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/DaumAddressSearchViewController.swift b/project/Projects/Presentation/Feature/Base/Sources/Address/DaumAddressSearchViewController.swift similarity index 97% rename from project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/DaumAddressSearchViewController.swift rename to project/Projects/Presentation/Feature/Base/Sources/Address/DaumAddressSearchViewController.swift index b372f41f..64b4ef3a 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/DaumAddressSearchViewController.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/Address/DaumAddressSearchViewController.swift @@ -10,8 +10,8 @@ import RxSwift import RxCocoa import DSKit import Entity -import PresentationCore import WebKit +import PresentationCore public enum AddressDataKey: String, CaseIterable { case address="address" @@ -27,7 +27,7 @@ public typealias Conformance = UIViewController & WKUIDelegate & WKNavigationDel public class DaumAddressSearchViewController: Conformance { - public var deleage: DaumAddressSearchDelegate? + public var delegate: DaumAddressSearchDelegate? // View private let navigationBar: NavigationBarType1 = { @@ -119,7 +119,7 @@ public extension DaumAddressSearchViewController { addressData[key] = address } } - deleage?.addressSearch(addressData: addressData) + delegate?.addressSearch(addressData: addressData) navigationController?.popViewController(animated: true) } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/BaseViewController.swift b/project/Projects/Presentation/Feature/Base/Sources/ViewController/BaseViewController.swift similarity index 80% rename from project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/BaseViewController.swift rename to project/Projects/Presentation/Feature/Base/Sources/ViewController/BaseViewController.swift index 44579952..dc4f5f6d 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/BaseViewController.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/ViewController/BaseViewController.swift @@ -1,6 +1,6 @@ // // BaseViewController.swift -// PresentationCore +// BaseFeature // // Created by choijunios on 7/23/24. // @@ -11,9 +11,9 @@ import Entity open class BaseViewController: UIViewController { } // MARK: Alert -extension BaseViewController { +public extension BaseViewController { - public func showAlert(vo: DefaultAlertContentVO) { + func showAlert(vo: DefaultAlertContentVO) { let alret = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) let close = UIAlertAction(title: "닫기", style: .default, handler: nil) alret.addAction(close) diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/DisposableViewController.swift b/project/Projects/Presentation/Feature/Base/Sources/ViewController/DisposableViewController.swift similarity index 92% rename from project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/DisposableViewController.swift rename to project/Projects/Presentation/Feature/Base/Sources/ViewController/DisposableViewController.swift index 28a60fda..39ff868c 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/DisposableViewController.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/ViewController/DisposableViewController.swift @@ -1,6 +1,6 @@ // // DisposableViewController.swift -// PresentationCore +// BaseFeature // // Created by choijunios on 6/30/24. // diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift similarity index 100% rename from project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift rename to project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift similarity index 94% rename from project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift rename to project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift index b298c04c..26f6c8f8 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift @@ -1,6 +1,6 @@ // // CTAButton.swift -// PresentationCore +// BaseFeature // // Created by choijunios on 7/6/24. // diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift similarity index 90% rename from project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift rename to project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift index ab9aec40..b37cfb62 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift @@ -1,6 +1,6 @@ // // DefaultAlertOutputable.swift -// PresentationCore +// BaseFeature // // Created by choijunios on 7/25/24. // diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/ViewModelType.swift b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/ViewModelType.swift similarity index 92% rename from project/Projects/Presentation/PresentationCore/Sources/ViewModelType/ViewModelType.swift rename to project/Projects/Presentation/Feature/Base/Sources/ViewModelType/ViewModelType.swift index dda8fd44..31032b70 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/ViewModelType.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/ViewModelType.swift @@ -1,6 +1,6 @@ // // ViewModelType.swift -// PresentationCore +// BaseFeature // // Created by choijunios on 7/6/24. // diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index 6ec3bf68..c820cef3 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -23,24 +23,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let store = TestStore() try! store.saveAuthToken( - accessToken: "", - refreshToken: "" + accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMjIyNzcxMywibmJmIjoxNzIyMjI3NzEzLCJleHAiOjE3MjIyMjgzMTMsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkwZmNjNS01OGI1LTdlOWYtYTE3NS1hZDUwMjZjMzI4M2EiLCJwaG9uZU51bWJlciI6IjAxMC00NDQ0LTUyMzIiLCJ1c2VyVHlwZSI6ImNlbnRlciJ9.gJXEtDruIRqYM9R6aszejnIDOm8VP6ROnrNqESIdssE", + refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMjIyNzcxMywibmJmIjoxNzIyMjI3NzEzLCJleHAiOjE3MjM0MzczMTMsInR5cGUiOiJSRUZSRVNIX1RPS0VOIiwidXNlcklkIjoiMDE5MGZjYzUtNThiNS03ZTlmLWExNzUtYWQ1MDI2YzMyODNhIiwidXNlclR5cGUiOiJjZW50ZXIifQ.EtV-qojoAl-H7VVm-Dr2tYf6Hkbx3OdwbsxduAOFf6I" ) let useCase = DefaultCenterProfileUseCase( repository: DefaultUserProfileRepository(store) ) - let viewModel = CenterProfileViewModel( - useCase: useCase - ) - - let vc = CenterProfileViewController() + let navigationController = UINavigationController() + navigationController.setNavigationBarHidden(true, animated: false) - vc.bind(viewModel: viewModel) + let coordinator = RegisterCenterInfoCoordinator( + profileUseCase: useCase, + navigationController: navigationController + ) window = UIWindow(windowScene: windowScene) - window?.rootViewController = vc + window?.rootViewController = navigationController window?.makeKeyAndVisible() + + coordinator.start() } } diff --git a/project/Projects/Presentation/Feature/Center/Project.swift b/project/Projects/Presentation/Feature/Center/Project.swift index 5c87b31d..c9aee5ee 100644 --- a/project/Projects/Presentation/Feature/Center/Project.swift +++ b/project/Projects/Presentation/Feature/Center/Project.swift @@ -28,6 +28,7 @@ let project = Project( resources: ["Resources/**"], dependencies: [ // Presentation + D.Presentation.BaseFeature, D.Presentation.PresentationCore, D.Presentation.DSKit, diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/CenterProfileCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/CenterProfileCoordinator.swift new file mode 100644 index 00000000..0d3c814a --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/CenterProfileCoordinator.swift @@ -0,0 +1,47 @@ +// +// CenterProfileCoordinator.swift +// CenterFeature +// +// Created by choijunios on 7/29/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +/// 내센터, 다른 센터를 모두 불러올 수 있습니다. +public class CenterProfileCoordinator: ChildCoordinator { + + public weak var viewControllerRef: UIViewController? + public weak var parent: CenterProfileRegisterCoordinatable? + + public let navigationController: UINavigationController + + public let viewModel: any CenterProfileViewModelable + + public init( + mode: ProfileMode, + profileUseCase: CenterProfileUseCase, + navigationController: UINavigationController + ) { + self.viewModel = CenterProfileViewModel(mode: mode, useCase: profileUseCase) + self.navigationController = navigationController + } + + public func start() { + let vc = CenterProfileViewController(coordinator: self) + vc.bind(viewModel: viewModel) + self.viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + parent?.removeChildCoordinator(self) + } + + func closeViewController() { + popViewController() + coordinatorDidFinish() + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/ProfileRegisterCompleteCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/ProfileRegisterCompleteCoordinator.swift new file mode 100644 index 00000000..6fa76389 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/ProfileRegisterCompleteCoordinator.swift @@ -0,0 +1,54 @@ +// +// ProfileRegisterCompleteCoordinator.swift +// CenterFeature +// +// Created by choijunios on 7/27/24. +// + +import UIKit +import Entity +import PresentationCore +import UseCaseInterface + +public class ProfileRegisterCompleteCoordinator: ChildCoordinator { + + public weak var viewControllerRef: UIViewController? + public weak var parent: CenterProfileRegisterCoordinatable? + + public let navigationController: UINavigationController + private let viewModel: ProfileRegisterCompleteViewModelable + + public init( + cardVO: CenterProfileCardVO, + navigationController: UINavigationController + ) { + self.viewModel = ProfileRegisterCompleteVM(centerCardVO: cardVO) + self.navigationController = navigationController + } + + deinit { + printIfDebug("\(String(describing: RegisterCenterInfoCoordinator.self))") + } + + public func start() { + let vc = ProfileRegisterCompleteVC(coordinator: self) + vc.bind(viewModel: viewModel) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + parent?.removeChildCoordinator(self) + } +} + +extension ProfileRegisterCompleteCoordinator { + + func showCenterProfile() { + parent?.showMyCenterProfile() + } + + func registerFinished() { + parent?.registerFinished() + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/RegisterCenterInfoCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/RegisterCenterInfoCoordinator.swift new file mode 100644 index 00000000..cbbd3a24 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/RegisterCenterInfoCoordinator.swift @@ -0,0 +1,57 @@ +// +// RegisterCenterInfoCoordinator.swift +// CenterFeature +// +// Created by choijunios on 7/27/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class RegisterCenterInfoCoordinator: ChildCoordinator { + + public weak var viewControllerRef: UIViewController? + public weak var parent: CenterProfileRegisterCoordinatable? + + public let navigationController: UINavigationController + + public let viewModel: RegisterCenterInfoViewModelable + + public init( + profileUseCase: CenterProfileUseCase, + navigationController: UINavigationController + ) { + self.viewModel = RegisterCenterInfoVM(profileUseCase: profileUseCase) + self.navigationController = navigationController + } + + deinit { + printIfDebug("\(String(describing: RegisterCenterInfoCoordinator.self))") + } + + public func start() { + let vc = RegisterCenterInfoVC(coordinator: self) + vc.bind(viewModel: viewModel) + + viewControllerRef = vc + + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + parent?.removeChildCoordinator(self) + } +} + +extension RegisterCenterInfoCoordinator { + + func showCompleteScreen(cardVO: CenterProfileCardVO) { + parent?.showCompleteScreen(cardVO: cardVO) + } + + func registerFinished() { + parent?.registerFinished() + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift index e2d6fa48..a280b591 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -11,12 +11,15 @@ import RxSwift import RxCocoa import DSKit import Entity +import BaseFeature public protocol CenterProfileViewModelable where Input: CenterProfileInputable, Output: CenterProfileOutputable { associatedtype Input associatedtype Output var input: Input { get } var output: Output? { get } + + var profileMode: ProfileMode { get } } public protocol CenterProfileInputable { @@ -42,6 +45,8 @@ public class CenterProfileViewController: BaseViewController { var viewModel: (any CenterProfileViewModelable)? + weak var coordinator: CenterProfileCoordinator? + let navigationBar: NavigationBarType1 = { let bar = NavigationBarType1(navigationTitle: "내 센터 정보") return bar @@ -132,37 +137,23 @@ public class CenterProfileViewController: BaseViewController { return textView }() - /// ☑️ "센토 사진" 라벨 ☑️ + /// ☑️ "센터 사진" 라벨 ☑️ let centerPictureLabel: IdleLabel = { let label = IdleLabel(typography: .Subtitle4) label.textString = "센터 사진" label.textColor = DSKitAsset.Colors.gray500.color return label }() - let centerImageView: UIImageView = { - let view = UIImageView() - view.layer.cornerRadius = 6 - view.clipsToBounds = true - view.backgroundColor = DSKitAsset.Colors.gray100.color - view.contentMode = .scaleAspectFill - - /// 이미지 뷰는 버튼을 자식으로 가지는데 기본적으로 isUserInteractionEnabled값이 fale라 자식 버튼에도 영향을 미친다. - /// 따라서 이터렉션이 필요한 자식이 있는 경우 명시적으로 아래 프로퍼티값을 true로 설정해야한다. - view.isUserInteractionEnabled = true + private lazy var centerImageView: ImageSelectView = { + let view = ImageSelectView(state: .editing, viewController: self) return view }() - let centerImageEditButton: UIButton = { - let btn = UIButton() - btn.setImage(DSKitAsset.Icons.editPhoto.image, for: .normal) - btn.isUserInteractionEnabled = true - return btn - }() - let edtingImage: PublishRelay = .init() + private let disposeBag = DisposeBag() - let disposeBag = DisposeBag() - - public init() { + public init(coordinator: CenterProfileCoordinator) { + + self.coordinator = coordinator super.init(nibName: nil, bundle: nil) @@ -230,10 +221,6 @@ public class CenterProfileViewController: BaseViewController { alignment: .fill ) - // 센터 이미지뷰 세팅 - centerImageView.addSubview(centerImageEditButton) - centerImageEditButton.translatesAutoresizingMaskIntoConstraints = false - let scrollView = UIScrollView() let divider = UIView() @@ -287,9 +274,6 @@ public class CenterProfileViewController: BaseViewController { locationIcon.widthAnchor.constraint(equalToConstant: 24), locationIcon.heightAnchor.constraint(equalTo: locationIcon.widthAnchor), - centerImageEditButton.widthAnchor.constraint(equalToConstant: 28), - centerImageEditButton.heightAnchor.constraint(equalTo: centerImageEditButton.widthAnchor), - centerIntroductionField.heightAnchor.constraint(equalToConstant: 156), ]) @@ -334,18 +318,15 @@ public class CenterProfileViewController: BaseViewController { centerImageView.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), centerImageView.heightAnchor.constraint(equalToConstant: 250), centerImageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -38), - - centerImageEditButton.trailingAnchor.constraint(equalTo: centerImageView.trailingAnchor, constant: -16), - centerImageEditButton.bottomAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: -16), ]) } - func setObservable() { + private func setObservable() { - centerImageEditButton - .rx.tap - .subscribe { [weak self] _ in - self?.showPhotoGalley() + navigationBar + .eventPublisher + .subscribe { [weak coordinator] _ in + coordinator?.closeViewController() } .disposed(by: disposeBag) } @@ -363,29 +344,35 @@ public class CenterProfileViewController: BaseViewController { .bind(to: input.readyToFetch) .disposed(by: disposeBag) - profileEditButton - .eventPublisher - .bind(to: input.editingButtonPressed) - .disposed(by: disposeBag) - - editingCompleteButton - .eventPublisher - .bind(to: input.editingFinishButtonPressed) - .disposed(by: disposeBag) - - centerPhoneNumeberField.rx.text - .compactMap { $0 } - .bind(to: input.editingPhoneNumber) - .disposed(by: disposeBag) - - centerIntroductionField.rx.text - .compactMap { $0 } - .bind(to: input.editingInstruction) - .disposed(by: disposeBag) - - edtingImage - .bind(to: input.selectedImage) - .disposed(by: disposeBag) + // 내 센터보기 상태인 경우(수정가능한 프로필 상태) + if case .myProfile = viewModel.profileMode { + + profileEditButton + .eventPublisher + .bind(to: input.editingButtonPressed) + .disposed(by: disposeBag) + + editingCompleteButton + .eventPublisher + .bind(to: input.editingFinishButtonPressed) + .disposed(by: disposeBag) + + centerPhoneNumeberField.rx.text + .compactMap { $0 } + .bind(to: input.editingPhoneNumber) + .disposed(by: disposeBag) + + centerIntroductionField.rx.text + .compactMap { $0 } + .bind(to: input.editingInstruction) + .disposed(by: disposeBag) + + centerImageView + .selectedImage + .compactMap { $0 } + .bind(to: input.selectedImage) + .disposed(by: disposeBag) + } // output guard let output = viewModel.output else { fatalError() } @@ -420,28 +407,50 @@ public class CenterProfileViewController: BaseViewController { output .displayingImage - .drive(centerImageView.rx.image) + .drive(centerImageView.displayingImage) .disposed(by: disposeBag) // MARK: Edit Mode - output - .isEditingMode - .drive { [weak self] in - guard let self else { return } - - centerPhoneNumeberField.isHidden = !$0 - centerPhoneNumeberLabel.isHidden = $0 - - centerIntroductionField.isHidden = !$0 - centerIntroductionLabel.isHidden = $0 - - centerImageEditButton.isHidden = !$0 - - editingCompleteButton.isHidden = !$0 - profileEditButton.isHidden = $0 - - } - .disposed(by: disposeBag) + if case .myProfile = viewModel.profileMode { + + output + .isEditingMode + .map { isEditing -> ImageSelectView.State in + isEditing ? .editing : .normal + } + .drive(centerImageView.state) + .disposed(by: disposeBag) + + output + .isEditingMode + .drive { [weak self] in + guard let self else { return } + + centerPhoneNumeberField.isHidden = !$0 + centerPhoneNumeberLabel.isHidden = $0 + + centerIntroductionField.isHidden = !$0 + centerIntroductionLabel.isHidden = $0 + + editingCompleteButton.isHidden = !$0 + profileEditButton.isHidden = $0 + } + .disposed(by: disposeBag) + + output + .editingValidation + .drive { _ in + // do something when editing success + } + .disposed(by: disposeBag) + } else { + // 수정 UI을 모두 끈다. + centerPhoneNumeberField.isHidden = true + centerIntroductionField.isHidden = true + editingCompleteButton.isHidden = true + profileEditButton.isHidden = true + centerImageView.state.accept(.normal) + } output .alert @@ -450,50 +459,7 @@ public class CenterProfileViewController: BaseViewController { } .disposed(by: disposeBag) - output - .editingValidation - .drive { _ in - // do something when editing success - } - .disposed(by: disposeBag) - // 바인딩 종료 bindFinished.accept(()) } } - -extension CenterProfileViewController { - - func showPhotoGalley() { - - let imagePickerVC = UIImagePickerController() - imagePickerVC.delegate = self - - if !UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { - - showAlert(vo: .init( - title: "오류", - message: "사진함을 열 수 없습니다.") - ) - - return - } - - imagePickerVC.sourceType = .photoLibrary - -// let modiaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) - present(imagePickerVC, animated: true) - } -} - -extension CenterProfileViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - - public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - - if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { - - edtingImage.accept(image) - picker.dismiss(animated: true) - } - } -} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RegisterCenterInfo/ProfileRegisterCompleteVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RegisterCenterInfo/ProfileRegisterCompleteVC.swift new file mode 100644 index 00000000..f41d76df --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RegisterCenterInfo/ProfileRegisterCompleteVC.swift @@ -0,0 +1,181 @@ +// +// ProfileRegisterCompleteVC.swift +// CenterFeature +// +// Created by choijunios on 7/27/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public protocol ProfileRegisterCompleteViewModelable { + // Output + var centerCardVO: Driver { get } +} + +public class ProfileRegisterCompleteVM: ProfileRegisterCompleteViewModelable { + + public var centerCardVO: Driver + + public init(centerCardVO: CenterProfileCardVO) { + + self.centerCardVO = Single + .just(centerCardVO) + .asDriver(onErrorJustReturn: .default) + } + +} + +public class ProfileRegisterCompleteVC: UIViewController { + + // Init + weak var coordinator: ProfileRegisterCompleteCoordinator? + + // View + let registerSuccessTitle: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + label.textString = "센터 회원으로 가입했어요." + return label + }() + // 추후 이미지 추가 예정 + let registerSuccessImage: UIView = { + let view = UIView() + view.backgroundColor = .cyan + return view + }() + + let centerProfileButton: CenterProfileButton = { + let profileBtn = CenterProfileButton( + nameString: "센터명", + locatonString: "위치" + ) + return profileBtn + }() + + // 하단 버튼 + let ctaButton: CTAButtonType1 = { + let button = CTAButtonType1(labelText: "시작하기") + return button + }() + + private let disposeBag = DisposeBag() + + public init(coordinator: ProfileRegisterCompleteCoordinator?) { + self.coordinator = coordinator + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + setAppearance() + setAutoLayout() + setObservable() + } + + private func setAppearance() { + view.backgroundColor = .white + view.layoutMargins = .init(top: 0, left: 20, bottom: 16, right: 20) + } + + private func setAutoLayout() { + + let titleImageSK = VStack( + [ + registerSuccessTitle, + registerSuccessImage, + ], + spacing: 20, + alignment: .center + ) + NSLayoutConstraint.activate([ + registerSuccessImage.widthAnchor.constraint(equalToConstant: 120), + registerSuccessImage.heightAnchor.constraint(equalTo: registerSuccessImage.widthAnchor), + ]) + + let profileButtonSK = VStack( + [ + { + let label = IdleLabel(typography: .Body3) + label.textString = "아래의 센터가 맞나요?" + label.textAlignment = .center + label.attrTextColor = DSKitAsset.Colors.gray300.color + return label + }(), + centerProfileButton + ], + spacing: 8, + alignment: .fill + ) + + let mainSK = VStack( + [ + titleImageSK, + profileButtonSK + ], + spacing: 44, + alignment: .fill + ) + + let mainBackground = UIView() + mainBackground.addSubview(mainSK) + mainSK.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + mainSK.centerYAnchor.constraint(equalTo: mainBackground.centerYAnchor), + mainSK.leadingAnchor.constraint(equalTo: mainBackground.leadingAnchor), + mainSK.trailingAnchor.constraint(equalTo: mainBackground.trailingAnchor), + ]) + + [ + mainBackground, + ctaButton, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + + mainBackground.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + mainBackground.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + mainBackground.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + mainBackground.bottomAnchor.constraint(equalTo: ctaButton.topAnchor), + + ctaButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + ctaButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + ctaButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor) + ]) + } + + private func setObservable() { + + centerProfileButton + .rx.tap + .subscribe { [weak coordinator] _ in + coordinator?.showCenterProfile() + } + .disposed(by: disposeBag) + + ctaButton + .eventPublisher + .subscribe { [weak coordinator] _ in + coordinator?.registerFinished() + } + .disposed(by: disposeBag) + } + + public func bind(viewModel vm: ProfileRegisterCompleteViewModelable) { + + vm + .centerCardVO + .drive { [centerProfileButton] vo in + centerProfileButton.nameLabel.textString = vo.name + centerProfileButton.addressLabel.textString = vo.location + } + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RegisterCenterInfo/RegisterCenterInfoVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RegisterCenterInfo/RegisterCenterInfoVC.swift new file mode 100644 index 00000000..cea06f21 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RegisterCenterInfo/RegisterCenterInfoVC.swift @@ -0,0 +1,722 @@ +// +// RegisterCenterInfoVC.swift +// AuthFeature +// +// Created by choijunios on 7/26/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +enum RegisterCenterInfoPage: Int, CaseIterable { + case nameAndPhoneNumber = 0 + case address = 1 + case imageAndIntroduction = 2 +} + +public protocol RegisterCenterInfoViewModelable { + // Input + var editingName: PublishRelay { get } + var editingCenterNumber: PublishRelay { get } + + var editingAddress: PublishRelay { get } + var editingDetailAddress: PublishRelay { get } + + var editingCenterIntroduction: PublishRelay { get } + var editingCenterImage: PublishRelay { get } + + var completeButtonPressed: PublishRelay { get } + + // Output + var nameAndNumberValidation: Driver? { get } + var addressValidation: Driver? { get } + var imageValidation: Driver? { get } + var profileRegisterSuccess: Driver? { get } + var alert: Driver? { get } +} + +fileprivate protocol CtaButtonIncludedView: UIView { + var ctaButton: CTAButtonType1 { get } + func bind(viewModel vm: RegisterCenterInfoViewModelable) +} + +public class RegisterCenterInfoVC: BaseViewController { + + // Init + + // Not init + /// 현재 스크린의 넓이를 의미합니다. + private var screenWidth: CGFloat { + guard let screenWidth = view.window?.windowScene?.screen.bounds.width else { + fatalError() + } + return screenWidth + } + + public weak var coordinator: RegisterCenterInfoCoordinator? + + private var pageViews: [CtaButtonIncludedView] = [] + private var pagesAreSetted = false + + var currentIndex: Int = 0 + + // For RC=1 + private var viewModel: RegisterCenterInfoViewModelable? + + // View + let navigationBar: NavigationBarType1 = { + let bar = NavigationBarType1( + navigationTitle: "센터 회원가입" + ) + return bar + }() + lazy var statusBar: ProcessStatusBar = { + + let view = ProcessStatusBar( + processCount: RegisterCenterInfoPage.allCases.count, + startIndex: 0 + ) + return view + }() + + let disposeBag = DisposeBag() + + public init(coordinator: RegisterCenterInfoCoordinator?) { + + self.coordinator = coordinator + + super.init(nibName: nil, bundle: nil) + + // View를 생성 + // View를 여기서 생성하는 이유는 bind매서드호출시(viewDidLoad이후) view들을 바인딩 시키기 위해서 입니다. + createPages() + setPagesLayoutAndObservable() + } + required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + // ViewController + setAppearance() + setLayout() + setObservable() + } + + /// 화면의 넓이를 안전하게 접근할 수 있는 시점, 화면 관련 속성들이 설정되어 있으므로 nil이 아닙니다. + public override func viewDidAppear(_ animated: Bool) { + if !pagesAreSetted { + pagesAreSetted = true + displayPageView() + } + } + + private func setAppearance() { + view.backgroundColor = .white + view.layoutMargins = .init(top: 0, left: 20, bottom: 0, right: 20) + } + + private func setObservable() { + + // 뒤로가기 바인딩 + navigationBar + .eventPublisher + .subscribe { [weak self] _ in + self?.prev() + } + .disposed(by: disposeBag) + } + + private func setLayout() { + + [ + navigationBar, + statusBar, + ].forEach { + view.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + NSLayoutConstraint.activate([ + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12), + navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12), + + statusBar.topAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 7), + statusBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + statusBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + ]) + } + + private func createPages() { + self.pageViews = RegisterCenterInfoPage.allCases.map { page in + switch page { + case .nameAndPhoneNumber: + NameAndPhoneNumberView() + case .address: + AddressView(viewController: self) + case .imageAndIntroduction: + ImageAndIntroductionView( + coordinator: coordinator, + viewController: self + ) + } + } + } + + private func setPagesLayoutAndObservable() { + + // 레이아웃 설정 + pageViews + .enumerated() + .forEach { index, subView in + view.addSubview(subView) + subView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + subView.topAnchor.constraint(equalTo: statusBar.bottomAnchor), + subView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + subView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + subView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + ]) + } + + // 첫번째 뷰를 최상단으로 + view.bringSubviewToFront(pageViews.first!) + + // 옵저버블 설정 + let observables = pageViews + .map { view in + view.ctaButton.eventPublisher + } + Observable + .merge(observables) + .subscribe(onNext: { [weak self] _ in + self?.next() + }) + .disposed(by: disposeBag) + } + + private func displayPageView() { + // 뷰들을 오른쪽으로 이동 + pageViews.forEach { view in + view.transform = .init(translationX: screenWidth, y: 0) + } + // 첫번째 뷰를 표시 + pageViews.first?.transform = .identity + } + + private func next(animated: Bool = true) { + + if let nextIndex = RegisterCenterInfoPage(rawValue: currentIndex+1)?.rawValue { + + // Status바 이동 + statusBar.moveToSignal.onNext(.next) + + let prevView: UIView? = currentIndex != -1 ? pageViews[currentIndex] : nil + let willShowView = pageViews[nextIndex] + + currentIndex = nextIndex + + UIView.animate(withDuration: animated ? 0.35 : 0.0) { [screenWidth, prevView, willShowView] in + + prevView?.transform = .init(translationX: -screenWidth, y: 0) + willShowView.transform = .identity + } + } + } + + private func prev(animated: Bool = true) { + if let nextIndex = RegisterCenterInfoPage(rawValue: currentIndex-1)?.rawValue { + + // Status바 이동 + statusBar.moveToSignal.onNext(.prev) + + let prevView = pageViews[currentIndex] + let willShowView = pageViews[nextIndex] + + currentIndex = nextIndex + + UIView.animate(withDuration: animated ? 0.35 : 0.0) { [screenWidth, prevView, willShowView] in + + prevView.transform = .init(translationX: screenWidth, y: 0) + willShowView.transform = .identity + } + } else { + + // 돌아가기, Coordinator호출 + coordinator?.registerFinished() + } + } + + public func bind(viewModel vm: RegisterCenterInfoViewModelable) { + + // RC=1 + self.viewModel = vm + + // Output + vm + .alert? + .drive { [weak self] vo in + self?.showAlert(vo: vo) + } + .disposed(by: disposeBag) + + // pageView에 ViewModel을 바인딩 + pageViews + .forEach { pv in + pv.bind(viewModel: vm) + } + } +} + +extension RegisterCenterInfoVC { + + // MARK: CenterInfoView (이름 + 센터 연락처) + class NameAndPhoneNumberView: UIView, CtaButtonIncludedView { + + // View + private let processTitle: IdleLabel = { + let label = IdleLabel(typography: .Heading2) + label.textString = "센터 정보를 입력해주세요." + label.textAlignment = .left + return label + }() + + + let nameField: IFType2 = { + let field = IFType2( + titleLabelText: "이름", + placeHolderText: "센터 이름을 입력해주세요." + ) + return field + }() + + let phoneNumberField: IFType2 = { + let field = IFType2( + titleLabelText: "센터 연락처", + placeHolderText: "지원자들의 연락을 받을 번호를 입력해주세요." + ) + return field + }() + + // 하단 버튼 + let ctaButton: CTAButtonType1 = { + + let button = CTAButtonType1(labelText: "다음") + button.setEnabled(false) + return button + }() + + init() { + super.init(frame: .zero) + setAppearance() + setLayout() + } + required init?(coder: NSCoder) { fatalError() } + + private func setAppearance() { + self.backgroundColor = .white + self.layoutMargins = .init(top: 32, left: 20, bottom: 0, right: 20) + } + + private func setLayout() { + + let inputStack = VStack( + [ + nameField, + phoneNumberField + ], + spacing: 28, + alignment: .fill + ) + + [ + processTitle, + inputStack, + ctaButton + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + + processTitle.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + processTitle.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + processTitle.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + + inputStack.topAnchor.constraint(equalTo: processTitle.bottomAnchor, constant: 32), + inputStack.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + inputStack.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + + ctaButton.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + ctaButton.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + ctaButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16) + ]) + } + + private let disposeBag = DisposeBag() + + public func bind(viewModel vm: RegisterCenterInfoViewModelable) { + // input + nameField + .eventPublisher + .bind(to: vm.editingName) + .disposed(by: disposeBag) + + phoneNumberField + .eventPublisher + .bind(to: vm.editingCenterNumber) + .disposed(by: disposeBag) + + // Output + vm + .nameAndNumberValidation? + .drive { [ctaButton] isValid in + ctaButton.setEnabled(isValid) + } + .disposed(by: disposeBag) + } + } + + // MARK: 센터주소 (도로명, 지번주소 + 상세주소) + class AddressView: UIView, DaumAddressSearchDelegate, CtaButtonIncludedView { + + // init + public weak var viewController: UIViewController? + + // View + private let processTitle: IdleLabel = { + let label = IdleLabel(typography: .Heading2) + label.textString = "센터 주소 정보를 입력해주세요." + label.textAlignment = .left + return label + }() + + private let addressSearchButton: TextButtonType2 = { + + let button = TextButtonType2(labelText: "도로명 주소를 입력해주세요.") + + return button + }() + + let detailAddressField: IFType2 = { + let field = IFType2( + titleLabelText: "상세 주소", + placeHolderText: "상세 주소를 입력해주세요. (예: 2층 204호)" + ) + return field + }() + + // 하단 버튼 + let ctaButton: CTAButtonType1 = { + + let button = CTAButtonType1(labelText: "다음") + button.setEnabled(false) + return button + }() + + // Observable + private let addressPublisher: PublishRelay = .init() + private let disposeBag = DisposeBag() + + init(viewController vc: UIViewController) { + self.viewController = vc + super.init(frame: .zero) + setAppearance() + setLayout() + setObservable() + } + required init?(coder: NSCoder) { fatalError() } + + private func setAppearance() { + self.backgroundColor = .white + self.layoutMargins = .init(top: 32, left: 20, bottom: 0, right: 20) + } + + private func setLayout() { + + let roadAddressStack = VStack( + [ + { + let label = IdleLabel(typography: .Subtitle4) + label.textString = "도로명주소" + label.textAlignment = .left + return label + }(), + addressSearchButton, + ], + spacing: 6, + alignment: .fill + ) + + let inputStack = VStack( + [ + roadAddressStack, + detailAddressField + ], + spacing: 28, + alignment: .fill + ) + + [ + processTitle, + inputStack, + ctaButton + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + + processTitle.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + processTitle.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + processTitle.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + + inputStack.topAnchor.constraint(equalTo: processTitle.bottomAnchor, constant: 32), + inputStack.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + inputStack.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + + ctaButton.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + ctaButton.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + ctaButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16) + ]) + } + + private func setObservable() { + + addressSearchButton + .eventPublisher + .subscribe { [weak self] _ in + self?.showDaumSearchView() + } + .disposed(by: disposeBag) + } + + private func showDaumSearchView() { + let vc = DaumAddressSearchViewController() + vc.delegate = self + vc.modalPresentationStyle = .fullScreen + viewController?.navigationController?.pushViewController(vc, animated: true) + } + + public func bind(viewModel vm: RegisterCenterInfoViewModelable) { + + // Input + addressPublisher + .bind(to: vm.editingAddress) + .disposed(by: disposeBag) + + detailAddressField + .uITextField.rx.text + .compactMap { $0 } + .bind(to: vm.editingDetailAddress) + .disposed(by: disposeBag) + + // output + vm + .addressValidation? + .drive(onNext: { [ctaButton] isValid in + ctaButton.setEnabled(isValid) + }) + .disposed(by: disposeBag) + } + + public func addressSearch(addressData: [AddressDataKey : String]) { + +// let address = addressData[.address] ?? "알 수 없는 주소" + let jibunAddress = addressData[.jibunAddress] ?? "알 수 없는 지번 주소" + let roadAddress = addressData[.roadAddress] ?? "알 수 없는 도로명 주소" + + addressSearchButton.label.textString = roadAddress + addressPublisher.accept( + AddressInformation( + roadAddress: roadAddress, + jibunAddress: jibunAddress + ) + ) + } + } + + // MARK: 센터 소개 (프로필 사진 + 센터소개) + class ImageAndIntroductionView: UIView, CtaButtonIncludedView { + + weak var coordinator: RegisterCenterInfoCoordinator? + + // init + public weak var viewController: UIViewController! + + // View + private let processTitle: IdleLabel = { + let label = IdleLabel(typography: .Heading2) + label.textString = "우리 센터를 소개해주세요!" + label.textAlignment = .left + return label + }() + private let subTitle: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.textString = "센터 소개글을 작성하면 보호사 매칭 성공률이 높아져요." + label.attrTextColor = DSKitAsset.Colors.gray300.color + label.textAlignment = .left + return label + }() + + /// 센터 소개를 수정하는 텍스트 필드 + private let centerIntroductionField: MultiLineTextField = { + let textView = MultiLineTextField( + typography: .Body3, + placeholderText: "추가적으로 요구사항이 있다면 작성해주세요." + ) + return textView + }() + + private lazy var centerImageView: ImageSelectView = { + let view = ImageSelectView(state: .editing, viewController: viewController) + return view + }() + + // 하단 버튼 + let ctaButton: CTAButtonType1 = { + let button = CTAButtonType1(labelText: "다음") + return button + }() + + // Observable + private let disposeBag = DisposeBag() + + init(coordinator: RegisterCenterInfoCoordinator?, viewController: UIViewController) { + self.coordinator = coordinator + self.viewController = viewController + super.init(frame: .zero) + setAppearance() + setLayout() + } + required init?(coder: NSCoder) { fatalError() } + + private func setAppearance() { + self.backgroundColor = .white + } + + private func setLayout() { + + let inputStackContents = [ + ("센터 소개", centerIntroductionField), + ("센터 사진", centerImageView), + ].map { (title: String, view: UIView) in + + let label = IdleLabel(typography: .Subtitle4) + label.textString = title + label.attrTextColor = DSKitAsset.Colors.gray500.color + label.textAlignment = .left + + view.translatesAutoresizingMaskIntoConstraints = false + + return VStack( + [ + label, + view + ], + spacing: 8, + alignment: .fill + ) + } + + NSLayoutConstraint.activate([ + + centerIntroductionField.heightAnchor.constraint(equalToConstant: 156), + centerImageView.heightAnchor.constraint(equalToConstant: 254), + ]) + + let inputStack = VStack( + inputStackContents, + spacing: 20, + alignment: .fill + ) + + let scrollView = UIScrollView() + [ + processTitle, + subTitle, + inputStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview($0) + } + let cg = scrollView.contentLayoutGuide + scrollView.layoutMargins = .init(top: 0, left: 20, bottom: 0, right: 20) + NSLayoutConstraint.activate([ + + processTitle.topAnchor.constraint(equalTo: cg.topAnchor, constant: 32), + processTitle.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + processTitle.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), + + subTitle.topAnchor.constraint(equalTo: processTitle.bottomAnchor, constant: 6), + subTitle.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + subTitle.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), + + inputStack.topAnchor.constraint(equalTo: subTitle.bottomAnchor, constant: 32), + inputStack.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + inputStack.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), + inputStack.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -24), + ]) + + [ + scrollView, + ctaButton, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: self.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: ctaButton.topAnchor), + + ctaButton.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + ctaButton.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor), + ctaButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16) + ]) + } + + public func bind(viewModel vm: RegisterCenterInfoViewModelable) { + + // Input + centerIntroductionField + .rx.text + .compactMap { $0 } + .bind(to: vm.editingCenterIntroduction) + .disposed(by: disposeBag) + + centerImageView + .selectedImage + .compactMap { $0 } + .bind(to: vm.editingCenterImage) + .disposed(by: disposeBag) + + // 완료버튼 + ctaButton + .eventPublisher + .bind(to: vm.completeButtonPressed) + .disposed(by: disposeBag) + + // Output + vm + .imageValidation? + .drive(centerImageView.displayingImage) + .disposed(by: disposeBag) + + vm + .profileRegisterSuccess? + .drive(onNext: { [weak coordinator] cardVO in + coordinator?.showCompleteScreen(cardVO: cardVO) + }) + .disposed(by: disposeBag) + } + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift index 7026d87e..c49e5d44 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -25,6 +25,8 @@ public class CenterProfileViewModel: CenterProfileViewModelable { public var input: Input public var output: Output? = nil + public let profileMode: ProfileMode + private var fetchedPhoneNumber: String? private var fetchedIntroduction: String? private var fetchedImage: UIImage? @@ -43,8 +45,9 @@ public class CenterProfileViewModel: CenterProfileViewModelable { ) } - public init(useCase: CenterProfileUseCase) { + public init(mode: ProfileMode, useCase: CenterProfileUseCase) { + self.profileMode = mode self.profileUseCase = useCase self.input = Input() @@ -52,8 +55,8 @@ public class CenterProfileViewModel: CenterProfileViewModelable { // MARK: fetch from server let profileRequestResult = input .readyToFetch - .flatMap { [unowned self] _ in - self.profileUseCase.getProfile() + .flatMap { [profileMode, profileUseCase] _ in + profileUseCase.getProfile(mode: profileMode) } .share() @@ -74,12 +77,14 @@ public class CenterProfileViewModel: CenterProfileViewModelable { .map { $0.roadNameAddress } .asDriver(onErrorJustReturn: "") + // 센터 소개는 필수값이 아님, 공백일 경우 필터링 let centerIntroductionDriver = profileRequestSuccess .map { [weak self] in let introduce = $0.introduce self?.fetchedIntroduction = introduce return introduce } + .filter { !$0.isEmpty } .asDriver(onErrorJustReturn: "") let centerPhoneNumberDriver = profileRequestSuccess @@ -137,7 +142,7 @@ public class CenterProfileViewModel: CenterProfileViewModelable { .map({ [unowned self] _ in checkModification() }) - .flatMap { [useCase] (inputs) in + .flatMap { [useCase, input] (inputs) in let (phoneNumber, introduction, imageInfo) = inputs @@ -146,8 +151,9 @@ public class CenterProfileViewModel: CenterProfileViewModelable { if let _ = introduction { printIfDebug("✅ 센터소개 변경되었음") } if let _ = imageInfo { printIfDebug("✅ 센터 이미지 변경되었음") } + // 전화번호는 무조건 포함시켜야 함으로 아래와 같이 포함합니다. return useCase.updateProfile( - phoneNumber: phoneNumber, + phoneNumber: phoneNumber ?? input.editingPhoneNumber.value, introduction: introduction, imageInfo: imageInfo ) diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RegisterCenterInfo/RegisterCenterInfoVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RegisterCenterInfo/RegisterCenterInfoVM.swift new file mode 100644 index 00000000..bb02cf59 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RegisterCenterInfo/RegisterCenterInfoVM.swift @@ -0,0 +1,160 @@ +// +// RegisterCenterInfoVM.swift +// AuthFeature +// +// Created by choijunios on 7/26/24. +// + +import UIKit +import RxCocoa +import RxSwift +import Entity +import PresentationCore +import UseCaseInterface + +public class RegisterCenterInfoVM: RegisterCenterInfoViewModelable { + + // Input + public var editingName: PublishRelay = .init() + public var editingCenterNumber: PublishRelay = .init() + public var editingAddress: PublishRelay = .init() + public var editingDetailAddress: PublishRelay = .init() + public var editingCenterIntroduction: PublishRelay = .init() + public var editingCenterImage: PublishRelay = .init() + public var completeButtonPressed: PublishRelay = .init() + + // Output + public var nameAndNumberValidation: Driver? = nil + public var addressValidation: Driver? = nil + public var introductionValidation: Driver? = nil + public var imageValidation: Driver? = nil + public var profileRegisterSuccess: Driver? = nil + public var alert: Driver? = nil + + // StatObject + private let stateObject = CenterProfileRegisterState() + + public init(profileUseCase useCase: CenterProfileUseCase) { + + // Set stream + self.nameAndNumberValidation = Observable + .combineLatest( + editingName, + editingCenterNumber + ) + .map { [stateObject] (name, phoneNumber) in + + printIfDebug("\(#function) 입력중인 센터이름: \(name)") + printIfDebug("\(#function) 입력중인 센터번호: \(phoneNumber)") + + stateObject.centerName = name + stateObject.officeNumber = phoneNumber + + return !name.isEmpty && !phoneNumber.isEmpty + } + .asDriver(onErrorJustReturn: false) + + self.addressValidation = Observable + .combineLatest( + editingAddress, + editingDetailAddress + ) + .map { [stateObject] (addressInfo, detailAd) in + + printIfDebug("\(#function) 입력중인 도려명 주소: \(addressInfo.roadAddress) \n 지번: \(addressInfo.jibunAddress)") + printIfDebug("\(#function) 입력중인 주소 디테일: \(detailAd)") + + let road = addressInfo.roadAddress + let jibun = addressInfo.jibunAddress + stateObject.roadNameAddress = road + stateObject.lotNumberAddress = jibun + stateObject.detailedAddress = detailAd + + return !road.isEmpty && !jibun.isEmpty && !detailAd.isEmpty + } + .asDriver(onErrorJustReturn: false) + + // 소개글은 필수값임 아님으로 별도로 조건 수행X + self.introductionValidation = editingCenterIntroduction + .map { [stateObject] intro in + stateObject.introduce = intro + return true + } + .asDriver(onErrorJustReturn: false) + + let imageValidation = editingCenterImage + .map { [unowned self] image in + let info = validateSelectedImage(image: image) + return (image: image, info: info) + } + .share() + + let imageValidationSuccess = imageValidation + .compactMap { (image, info) -> (UIImage, ImageUploadInfo)? in + if let info { return (image, info) } + return nil + } + + let imageValidationFailure = imageValidation + .filter { $0.info == nil } + .map { _ in + DefaultAlertContentVO( + title: "이미지 업로드 실패", + message: "지원하지 않는 파일 형식" + ) + } + + self.imageValidation = imageValidationSuccess + .map { [stateObject] (image, info) in + printIfDebug("\(#function) 입력중인 센터소개") + stateObject.imageInfo = info + return image + } + .asDriver(onErrorJustReturn: .init()) + + let profileRegisterResult = self.completeButtonPressed + .flatMap { [useCase, stateObject] _ in +#if DEBUG + return Single>.just(.success(())) +#endif + return useCase.registerCenterProfile(state: stateObject) + } + + profileRegisterSuccess = profileRegisterResult + .compactMap { $0.value } + .map { [stateObject] in + let cardVO = CenterProfileCardVO( + name: stateObject.centerName, + location: stateObject.roadNameAddress + ) + return cardVO + } + .asDriver(onErrorJustReturn: .default) + + let profileRegisterFailure = profileRegisterResult + .compactMap { $0.error } + .map { error in + DefaultAlertContentVO( + title: "센터정보 등록 싶패", + message: error.message + ) + } + + // Alert + self.alert = Observable + .merge( + imageValidationFailure, + profileRegisterFailure + ) + .asDriver(onErrorJustReturn: .default) + } + + func validateSelectedImage(image: UIImage) -> ImageUploadInfo? { + if let pngData = image.pngData() { + return .init(data: pngData, ext: "PNG") + } else if let jpegData = image.jpegData(compressionQuality: 1) { + return .init(data: jpegData, ext: "JPEG") + } + return nil + } +} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift index 4d4db893..ee06e9e8 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift @@ -14,13 +14,18 @@ public class RecruitmentManagementCoordinator: ChildCoordinator { public var navigationController: UINavigationController - public init(navigationController: UINavigationController) { + public weak var parent: CenterMainCoordinatable? + + public init( + parent: CenterMainCoordinatable, + navigationController: UINavigationController + ) { + self.parent = parent self.navigationController = navigationController } public func start() { - let vc = RecuitmentManagementVC() - + let vc = RecuitmentManagementVC(coordinator: self) navigationController.pushViewController(vc, animated: false) } @@ -28,3 +33,10 @@ public class RecruitmentManagementCoordinator: ChildCoordinator { } } + +extension RecruitmentManagementCoordinator { + + func showCenterRegisterScreen() { + parent?.centerProfileRegister() + } +} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift index f9dcd214..cd227753 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift @@ -6,10 +6,16 @@ // import UIKit +import RxCocoa +import RxSwift public class RecuitmentManagementVC: UIViewController { - public init() { + weak var coordinator: RecruitmentManagementCoordinator? + + public init(coordinator: RecruitmentManagementCoordinator) { + self.coordinator = coordinator + super.init(nibName: nil, bundle: nil) setAppearacne() @@ -17,18 +23,38 @@ public class RecuitmentManagementVC: UIViewController { public required init?(coder: NSCoder) { fatalError() } + let dispoesBag = DisposeBag() + private func setAppearacne() { view.backgroundColor = .white let label = UILabel() label.text = "공고 관리" - label.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(label) + let button = UIButton() + button.setTitle("센터정보 등록", for: .normal) + button.setTitleColor(.black, for: .normal) + button.isUserInteractionEnabled = true + + button.rx.tap + .subscribe { [weak coordinator] _ in + coordinator?.showCenterRegisterScreen() + } + .disposed(by: dispoesBag) + [ + label, + button, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: view.centerXAnchor), label.centerYAnchor.constraint(equalTo: view.centerYAnchor), + + button.centerXAnchor.constraint(equalTo: view.centerXAnchor), + button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 15), ]) } } diff --git a/project/Projects/Presentation/Feature/Worker/Project.swift b/project/Projects/Presentation/Feature/Worker/Project.swift index 67367400..ca35236a 100644 --- a/project/Projects/Presentation/Feature/Worker/Project.swift +++ b/project/Projects/Presentation/Feature/Worker/Project.swift @@ -28,6 +28,7 @@ let project = Project( resources: ["Resources/**"], dependencies: [ // Presentation + D.Presentation.BaseFeature, D.Presentation.PresentationCore, D.Presentation.DSKit, diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/EditWorkerProfileViewController.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/profile/EditWorkerProfileViewController.swift index 84a339c0..920557ff 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/EditWorkerProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/profile/EditWorkerProfileViewController.swift @@ -11,6 +11,7 @@ import RxSwift import RxCocoa import DSKit import Entity +import BaseFeature public class EditWorkerProfileViewController: DisposableViewController { diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/WorkerProfileViewController.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/profile/WorkerProfileViewController.swift index dee63aff..22b9458e 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/WorkerProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/profile/WorkerProfileViewController.swift @@ -11,6 +11,7 @@ import RxSwift import RxCocoa import DSKit import Entity +import BaseFeature public protocol WorkerProfileViewModelable where Input: WorkerProfileInputable, Output: WorkerProfileOutputable { associatedtype Input diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift index be593031..2d555af9 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift @@ -52,7 +52,7 @@ public extension ParentCoordinator { func clearChildren() { - print(self, childCoordinators, navigationController.viewControllers) + printIfDebug(self, childCoordinators, navigationController.viewControllers) let lastCoordinator = childCoordinators.popLast() diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Main/CenterMainCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Main/CenterMainCoordinatable.swift new file mode 100644 index 00000000..e43c9a15 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Main/CenterMainCoordinatable.swift @@ -0,0 +1,12 @@ +// +// CenterMainCoordinatable.swift +// PresentationCore +// +// Created by choijunios on 7/27/24. +// + +import Foundation + +public protocol CenterMainCoordinatable: ParentCoordinator { + func centerProfileRegister() +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Profile/CenterProfileRegisterCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Profile/CenterProfileRegisterCoordinatable.swift new file mode 100644 index 00000000..a08e4579 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Profile/CenterProfileRegisterCoordinatable.swift @@ -0,0 +1,16 @@ +// +// CenterProfileRegisterCoordinatable.swift +// PresentationCore +// +// Created by choijunios on 7/27/24. +// + +import Foundation +import Entity + +public protocol CenterProfileRegisterCoordinatable: ParentCoordinator { + + func registerFinished() + func showCompleteScreen(cardVO: CenterProfileCardVO) + func showMyCenterProfile() +} diff --git a/project/graph.png b/project/graph.png index acc71a64..7b816840 100644 Binary files a/project/graph.png and b/project/graph.png differ