diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift index f681ed47..08b34bbc 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift @@ -73,7 +73,8 @@ class CenterMainCoordinator: CenterMainCoordinatable { dependency: .init( parent: self, navigationController: navigationController, - workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self) + workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self), + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) ) @@ -123,9 +124,13 @@ extension CenterMainCoordinator { /// 공고등록창을 표시합니다. func registerRecruitmentPost() { - let coordinator = RegisterRecruitmentPostCoordinator(dependency: .init( - navigationController: navigationController, - injector: injector) + let coordinator = RegisterRecruitmentPostCoordinator( + dependency: .init( + navigationController: navigationController, + recruitmentPostUseCase: injector.resolve( + RecruitmentPostUseCase.self + ) + ) ) coordinator.parent = self addChildCoordinator(coordinator) diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift index ad77cf20..71de734b 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift @@ -10,67 +10,3 @@ import DSKit import PresentationCore import CenterFeature import UseCaseInterface - - -class RegisterRecruitmentPostCoordinator: RegisterRecruitmentPostCoordinatable { - - struct Dependency { - let navigationController: UINavigationController - let injector: Injector - } - - var childCoordinators: [Coordinator] = [] - - var parent: ParentCoordinator? - - var navigationController: UINavigationController - let injector: Injector - - var viewModel: RegisterRecruitmentPostViewModelable! - - init(dependency: Dependency) { - self.navigationController = dependency.navigationController - self.injector = dependency.injector - } - - func start() { - self.viewModel = RegisterRecruitmentPostVM( - recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) - ) - - let coordinator = RegisterRecruitmentCoordinator( - viewModel: viewModel, - navigationController: navigationController - ) - coordinator.parent = self - addChildCoordinator(coordinator) - coordinator.start() - } -} - -extension RegisterRecruitmentPostCoordinator { - - func showOverViewScreen() { - let coordinator = PostOverviewCoordinator( - viewModel: viewModel, - navigationController: navigationController - ) - coordinator.parent = self - addChildCoordinator(coordinator) - coordinator.start() - } - - func showRegisterCompleteScreen() { - let coordinator = RegisterCompleteCoordinator( - navigationController: navigationController - ) - coordinator.parent = self - addChildCoordinator(coordinator) - coordinator.start() - } - - func registerFinished() { - clearChildren() - parent?.removeChildCoordinator(self) - } -} diff --git a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift index b763b42a..090e05e4 100644 --- a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift +++ b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift @@ -13,60 +13,95 @@ import Foundation public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { - let service: RecruitmentPostService = .init() + private var service: RecruitmentPostService = .init() - public init() { } + public init(_ store: KeyValueStore? = nil) { + if let store { + self.service = RecruitmentPostService(keyValueStore: store) + } + } + + public func registerPost(bundle: RegisterRecruitmentPostBundle) -> RxSwift.Single { + + let encodedData = try! JSONEncoder().encode(bundle.toDTO()) + + return service.request(api: .registerPost(postData: encodedData), with: .withToken) + .map { _ in () } + } + + public func getPostDetailForCenter(id: String) -> RxSwift.Single { + + service.request(api: .postDetail(id: id, userType: .center), with: .withToken) + .map(RecruitmentPostFetchDTO.self) + .map { dto in + dto.toEntity() + } + } - public func registerPost(input1: Entity.WorkTimeAndPayStateObject, input2: Entity.AddressInputStateObject, input3: Entity.CustomerInformationStateObject, input4: Entity.CustomerRequirementStateObject, input5: Entity.ApplicationDetailStateObject) -> RxSwift.Single { + public func editPostDetail(id: String, bundle: RegisterRecruitmentPostBundle) -> RxSwift.Single { + + let encodedData = try! JSONEncoder().encode(bundle.toDTO()) + + return service.request( + api: .editPost(id: id, postData: encodedData), + with: .withToken + ).map { _ in () } + } +} + +fileprivate extension RegisterRecruitmentPostBundle { + + func toDTO() -> RecruitmentPostRegisterDTO { // WorkTimeAndPayment // - all required - let weekDays: [String] = input1.selectedDays + let weekDays: [String] = workTimeAndPay.selectedDays .filter ({ (key, value) in value }).keys .map { day in day.dtoFormString } - let startTime: String = input1.workStartTime!.dtoFormString - let endTime: String = input1.workEndTime!.dtoFormString - let paymentType: String = input1.paymentType!.dtoFormString - let paymentAmount: Int = .init(input1.paymentAmount)! + let startTime: String = workTimeAndPay.workStartTime!.dtoFormString + let endTime: String = workTimeAndPay.workEndTime!.dtoFormString + let paymentType: String = workTimeAndPay.paymentType!.dtoFormString + let paymentAmount: Int = .init(workTimeAndPay.paymentAmount)! // AddressInputStateObject // - all required - let roadNameAddress: String = input2.addressInfo!.roadAddress - let lotNumberAddress: String = input2.addressInfo!.jibunAddress + let roadNameAddress: String = addressInfo.addressInfo!.roadAddress + let lotNumberAddress: String = addressInfo.addressInfo!.jibunAddress // CustomerInformationStateObject // - required - let clientName: String = input3.name - let gender: String = input3.gender!.dtoFormString - let birthYear: Int = .init(input3.birthYear)! - let weight: Int = .init(input3.weight)! - let careLevel: Int = input3.careGrade!.dtoFormInt - let mentalStatus: String = input3.cognitionState!.dtoFormString + let clientName: String = customerInformation.name + let gender: String = customerInformation.gender!.dtoFormString + let birthYear: Int = .init(customerInformation.birthYear)! + let weight: Int? = .init(customerInformation.weight) ?? nil + let careLevel: Int = customerInformation.careGrade!.dtoFormInt + let mentalStatus: String = customerInformation.cognitionState!.dtoFormString // - optional - let disease: String = input3.deceaseDescription + let disease: String? = customerInformation.deceaseDescription.isEmpty ? nil : customerInformation.deceaseDescription // CustomerRequirementStateObject // - required - let isMealAssistance: Bool = input4.mealSupportNeeded! - let isBowelAssistance: Bool = input4.toiletSupportNeeded! - let isWalkingAssistance: Bool = input4.movingSupportNeeded! + let isMealAssistance: Bool = customerRequirement.mealSupportNeeded! + let isBowelAssistance: Bool = customerRequirement.toiletSupportNeeded! + let isWalkingAssistance: Bool = customerRequirement.movingSupportNeeded! // - optional - let extraRequirement: String = input4.additionalRequirement - let lifeAssistance: [String] = input4.dailySupportTypeNeeds + let extraRequirement: String? = customerRequirement.additionalRequirement.isEmpty ? nil :customerRequirement.additionalRequirement + let lifAssistanceList = customerRequirement.dailySupportTypeNeeds .filter ({ $1 }).keys .map { type in type.dtoFormString } + let lifeAssistance: [String] = lifAssistanceList.isEmpty ? ["NONE"] : lifAssistanceList // ApplicationDetailStateObject // - required - let isExperiencePreferred = input5.experiencePreferenceType! == .beginnerPossible ? false : true - let applyMethod = input5.applyType + let isExperiencePreferred = applicationDetail.experiencePreferenceType! == .beginnerPossible ? false : true + let applyMethod = applicationDetail.applyType .filter ({ $1 }).keys .map { type in type.dtoFormString } - let applyDeadlineType = input5.applyDeadlineType!.dtoFormString - let applyDeadline = input5.deadlineDate!.dtoFormString + let applyDeadlineType = applicationDetail.applyDeadlineType!.dtoFormString + let applyDeadline = applicationDetail.deadlineDate!.dtoFormString - let dto = RecruitmentPostDTO( + let dto = RecruitmentPostRegisterDTO( isMealAssistance: isMealAssistance, isBowelAssistance: isBowelAssistance, isWalkingAssistance: isWalkingAssistance, @@ -91,11 +126,7 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { applyDeadline: applyDeadline, applyDeadlineType: applyDeadlineType ) - - let encodedData = try! JSONEncoder().encode(dto) - - return service.request(api: .registerPost(postData: encodedData), with: .withToken) - .map { _ in () } + return dto } } diff --git a/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift b/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift index 754abe4f..13b7d1e4 100644 --- a/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift @@ -7,11 +7,18 @@ import Moya import Foundation +import Entity public enum RcruitmentPostAPI { + // Common + case postDetail(id: String, userType: UserType) + // Center case registerPost(postData: Data) + case editPost(id: String, postData: Data) + case removePost(id: String) + case closePost(id: String) } extension RcruitmentPostAPI: BaseAPI { @@ -22,15 +29,31 @@ extension RcruitmentPostAPI: BaseAPI { public var path: String { switch self { + case .postDetail(let id, let userType): + "/\(id)/\(userType.pathUri)" case .registerPost: "" + case .editPost(let id, _): + "/\(id)" + case .removePost(let id): + "/\(id)" + case .closePost(let id): + "/\(id)/end" } } public var method: Moya.Method { switch self { + case .postDetail: + .get case .registerPost: .post + case .editPost: + .patch + case .removePost: + .delete + case .closePost: + .patch } } @@ -45,6 +68,10 @@ extension RcruitmentPostAPI: BaseAPI { switch self { case .registerPost(let bodyData): .requestData(bodyData) + case .editPost(_, let editData): + .requestData(editData) + default: + .requestPlain } } } diff --git a/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift index d4109354..fd106d6c 100644 --- a/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift +++ b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift @@ -8,7 +8,7 @@ import Foundation import Entity -public struct RecruitmentPostDTO: Codable { +public struct RecruitmentPostRegisterDTO: Codable { public let isMealAssistance, isBowelAssistance, isWalkingAssistance, isExperiencePreferred: Bool public let weekdays: [String] public let startTime, endTime, payType: String @@ -51,3 +51,276 @@ public struct RecruitmentPostDTO: Codable { self.applyDeadlineType = applyDeadlineType } } + + +public struct RecruitmentPostFetchDTO: Codable { + public let id: String + public let isMealAssistance, isBowelAssistance, isWalkingAssistance, isExperiencePreferred: Bool + public let weekdays: [String] + public let startTime, endTime, payType: String + public let payAmount: Int + public let roadNameAddress, lotNumberAddress, clientName, gender: String + public let age: Int + public let weight: Int? + public let careLevel: Int + public let mentalStatus: String + public let disease: String? + public let lifeAssistance: [String]? + public let extraRequirement: String? + public let applyMethod: [String] + public let applyDeadline: String? + public let applyDeadlineType: String + + public init( + id: String, + isMealAssistance: Bool, + isBowelAssistance: Bool, + isWalkingAssistance: Bool, + isExperiencePreferred: Bool, + weekdays: [String], + startTime: String, + endTime: String, + payType: String, + payAmount: Int, + roadNameAddress: String, + lotNumberAddress: String, + clientName: String, + gender: String, + age: Int, + weight: Int?, + careLevel: Int, + mentalStatus: String, + disease: String?, + lifeAssistance: [String]?, + extraRequirement: String?, + applyMethod: [String], + applyDeadline: String?, + applyDeadlineType: String + ) { + self.id = id + self.isMealAssistance = isMealAssistance + self.isBowelAssistance = isBowelAssistance + self.isWalkingAssistance = isWalkingAssistance + self.isExperiencePreferred = isExperiencePreferred + self.weekdays = weekdays + self.startTime = startTime + self.endTime = endTime + self.payType = payType + self.payAmount = payAmount + self.roadNameAddress = roadNameAddress + self.lotNumberAddress = lotNumberAddress + self.clientName = clientName + self.gender = gender + self.age = age + self.weight = weight + self.careLevel = careLevel + self.mentalStatus = mentalStatus + self.disease = disease + self.lifeAssistance = lifeAssistance + self.extraRequirement = extraRequirement + self.applyMethod = applyMethod + self.applyDeadline = applyDeadline + self.applyDeadlineType = applyDeadlineType + } + + public func toEntity() -> RegisterRecruitmentPostBundle { + let workTimeAndPay: WorkTimeAndPayStateObject = .init() + weekdays.forEach({ dayText in + let entity = WorkDay.toEntity(text: dayText) + workTimeAndPay.selectedDays[entity] = true + }) + workTimeAndPay.workStartTime = IdleDateComponent.toEntity(text: startTime) + workTimeAndPay.workEndTime = IdleDateComponent.toEntity(text: endTime) + workTimeAndPay.paymentType = PaymentType.toEntity(text: payType) + workTimeAndPay.paymentAmount = String(payAmount) + + let addressInfo: AddressInputStateObject = .init() + addressInfo.addressInfo = .init( + roadAddress: roadNameAddress, + jibunAddress: lotNumberAddress + ) + + let customerInfo: CustomerInformationStateObject = .init() + customerInfo.name = clientName + customerInfo.gender = Gender.toEntity(text: gender) + + let currentYear = Calendar.current.component(.year, from: Date()) + customerInfo.birthYear = String(currentYear - age) + customerInfo.weight = (weight == nil) ? String(weight!) : "" + customerInfo.careGrade = CareGrade(rawValue: careLevel-1)! + + customerInfo.cognitionState = CognitionDegree.toEntity(text: mentalStatus) + customerInfo.deceaseDescription = disease ?? "" + + let customerRequirement: CustomerRequirementStateObject = .init() + customerRequirement.mealSupportNeeded = isMealAssistance + customerRequirement.toiletSupportNeeded = isBowelAssistance + customerRequirement.movingSupportNeeded = isWalkingAssistance + customerRequirement.additionalRequirement = extraRequirement ?? "" + lifeAssistance?.forEach({ str in + let entity = DailySupportType.toEntity(text: str) + customerRequirement.dailySupportTypeNeeds[entity] = true + }) + + let applicationDetail: ApplicationDetailStateObject = .init() + applicationDetail.experiencePreferenceType = isExperiencePreferred ? .experiencedFirst : .beginnerPossible + applyMethod.forEach { type in + let entity = ApplyType.toEntity(text: type) + applicationDetail.applyType[entity] = true + } + applicationDetail.applyDeadlineType = ApplyDeadlineType.toEntity(text: applyDeadlineType) + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + applicationDetail.deadlineDate = applyDeadline != nil ? dateFormatter.date(from: applyDeadline!) : nil + + return .init( + workTimeAndPay: workTimeAndPay, + customerRequirement: customerRequirement, + customerInformation: customerInfo, + applicationDetail: applicationDetail, + addressInfo: addressInfo + ) + } +} + +fileprivate extension ApplyType { + static func toEntity(text: String) -> ApplyType { + switch text { + case "CALLING": + return .phoneCall + case "MESSAGE": + return .message + case "APP": + return .app + default: + print("ApplyType 디코딩 에러") + return .phoneCall // 예외 처리용으로 `unknown` 또는 적절한 기본 값을 사용하세요. + } + } +} + +fileprivate extension ApplyDeadlineType { + static func toEntity(text: String) -> ApplyDeadlineType { + switch text { + case "UNLIMITED": + return .untilApplicationFinished + case "LIMITED": + return .specificDate + default: + print("ApplyDeadlineType 디코딩 에러") + return .untilApplicationFinished // 예외 처리용으로 `unknown` 또는 적절한 기본 값을 사용하세요. + } + } +} + +fileprivate extension DailySupportType { + static func toEntity(text: String) -> DailySupportType { + switch text { + case "CLEANING": + return .cleaning + case "LAUNDRY": + return .laundry + case "WALKING": + return .walking + case "HEALTH": + return .exerciseSupport + case "TALKING": + return .listener + default: + print("DailySupportType 디코딩 에러") + return .cleaning // 예외 처리용으로 `unknown` 또는 적절한 기본 값을 사용하세요. + } + } +} + +fileprivate extension Gender { + static func toEntity(text: String) -> Gender { + switch text { + case "MAN": + return .male + case "WOMAN": + return .female + default: + print("Gender 디코딩 에러") + return .notDetermined + } + } +} + +fileprivate extension PaymentType { + + static func toEntity(text: String) -> PaymentType { + switch text { + case "HOURLY": + return .hourly + case "WEEKLY": + return .weekly + case "MONTHLY": + return .monthly + default: + print("PaymentType 디코딩 에러") + return .hourly + } + } +} + +fileprivate extension IdleDateComponent { + + static func toEntity(text: String) -> IdleDateComponent { + let timeArr = text.split(separator: ":") + let hour = timeArr[0] + let minute = timeArr[1] + let intHour = Int(hour) ?? 0 + + let isPM = intHour >= 13 + + return .init( + part: isPM ? .PM : .AM, + hour: String(intHour - (isPM ? 12 : 0)), + minute: String(minute) + ) + } +} + +fileprivate extension CognitionDegree { + static func toEntity(text: String) -> CognitionDegree { + switch text { + case "NORMAL": + return .stable + case "EARLY_STAGE": + return .earlyStage + case "OVER_MIDDLE_STAGE": + return .overEarlyStage + default: + print("CognitionDegree 디코딩 에러") + return .earlyStage + } + } +} + +fileprivate extension WorkDay { + + static func toEntity(text: String) -> WorkDay { + + switch text { + case "MONDAY": + return WorkDay.mon + case "TUESDAY": + return WorkDay.tue + case "WEDNESDAY": + return WorkDay.wed + case "THURSDAY": + return WorkDay.thu + case "FRIDAY": + return WorkDay.fri + case "SATURDAY": + return WorkDay.sat + case "SUNDAY": + return WorkDay.sun + default: + print("WorkDay 디코딩 에러") + return WorkDay.sun + } + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift index 5ab7381e..9603fb55 100644 --- a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift @@ -30,9 +30,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { convert(task: self.repository .requestPhoneNumberAuthentication(phoneNumber: phoneNumber) .map { _ in phoneNumber } - ) { [unowned self] error in - toDomainError(error: error) - } + ) } public func checkPhoneNumberIsValid(phoneNumber: String) -> Bool { @@ -46,9 +44,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { convert(task: repository .authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) .map({ _ in phoneNumber }) - ) { [unowned self] error in - toDomainError(error: error) - } + ) } // MARK: 사업자 번호 인증 @@ -56,9 +52,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { convert(task: repository .requestBusinessNumberAuthentication(businessNumber: businessNumber) .map({ vo in (businessNumber, vo) }) - ) { [unowned self] error in - toDomainError(error: error) - } + ) } public func checkBusinessNumberIsValid(businessNumber: String) -> Bool { @@ -80,9 +74,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { convert(task: repository .requestCheckingIdDuplication(id: id) .map({ _ in id }) - ) { [unowned self] error in - toDomainError(error: error) - } + ) } public func checkPasswordIsValid(password: String) -> Bool { diff --git a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift index 0e1ca968..65c7f854 100644 --- a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift @@ -28,27 +28,23 @@ public class DefaultAuthUseCase: AuthUseCase { businessNumber: registerState.businessNumber, id: registerState.id, password: registerState.password - )) { [unowned self] error in toDomainError(error: error) } + )) } /// 센터 로그인 실행 public func loginCenterAccount(id: String, password: String) -> Single> { - convert(task: repository.requestCenterLogin(id: id, password: password)) { [unowned self] error in - toDomainError(error: error) - } + convert(task: repository.requestCenterLogin(id: id, password: password)) } /// 요양 보호사 회원가입 실행 public func registerWorkerAccount(registerState: WorkerRegisterState) -> Single> { convert( - task: repository.requestRegisterWorkerAccount(registerState: registerState)) { [unowned self] error in toDomainError(error: error) - } + task: repository.requestRegisterWorkerAccount(registerState: registerState)) } /// 요양 보호사 로그인 실행 public func loginWorkerAccount(phoneNumber: String, authNumber: String) -> Single> { convert( - task: repository.requestWorkerLogin(phoneNumber: phoneNumber, authNumber: authNumber)) { [unowned self] error in toDomainError(error: error) - } + task: repository.requestWorkerLogin(phoneNumber: phoneNumber, authNumber: authNumber)) } } diff --git a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift index 50ff28a0..5c95aa67 100644 --- a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift @@ -29,14 +29,28 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { return convert( task: repository.registerPost( - input1: inputs.workTimeAndPay, - input2: inputs.addressInfo, - input3: inputs.customerInformation, - input4: inputs.customerRequirement, - input5: inputs.applicationDetail + bundle: inputs ) - .map({ _ in Void() }) ) { [unowned self] error in - toDomainError(error: error) + ) + } + + public func editRecruitmentPost(id: String, inputs: Entity.RegisterRecruitmentPostBundle) -> RxSwift.Single> { + + if inputs.applicationDetail.applyDeadlineType == .untilApplicationFinished { + let oneMonthLater = Calendar.current.date(byAdding: .month, value: 1, to: Date()) + inputs.applicationDetail.deadlineDate = oneMonthLater } + + return convert( + task: repository.editPostDetail( + id: id, + bundle: inputs + ) + ) + } + + + public func getPostDetailForCenter(id: String) -> RxSwift.Single> { + convert(task: repository.getPostDetailForCenter(id: id)) } } diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift index 5f243ccf..5d1fe9ef 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift @@ -20,13 +20,11 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { } public func getProfile(mode: ProfileMode) -> Single> { - convert(task: repository.getCenterProfile(mode: mode)) { [unowned self] error in - toDomainError(error: error) - } + convert(task: repository.getCenterProfile(mode: mode)) } public func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> { - + var updateText: Single! var updateImage: Single! @@ -61,7 +59,7 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { } return .error(error) } - + let uploadImageResult = updateImage .catch { error in if let httpExp = error as? HTTPResponseException { @@ -84,9 +82,7 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { .map { _ in () } .asSingle() - return convert(task: task) { [unowned self] error in - toDomainError(error: error) - } + return convert(task: task) } public func registerCenterProfile(state: CenterProfileRegisterState) -> Single> { @@ -140,8 +136,6 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { .map { _ in () } .asSingle() - return convert(task: task) { [unowned self] error in - toDomainError(error: error) - } + return convert(task: task) } } diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift index 449167af..faa488b3 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift @@ -20,9 +20,7 @@ public class DefaultWorkerProfileUseCase: WorkerProfileUseCase { } public func getProfile(mode: ProfileMode) -> Single> { - convert(task: repository.getWorkerProfile(mode: mode)) { [unowned self] error in - toDomainError(error: error) - } + convert(task: repository.getWorkerProfile(mode: mode)) } public func updateProfile(stateObject: WorkerProfileStateObject, imageInfo: ImageUploadInfo?) -> Single> { @@ -79,8 +77,6 @@ public class DefaultWorkerProfileUseCase: WorkerProfileUseCase { .map { _ in () } .asSingle() - return convert(task: task) { [unowned self] error in - toDomainError(error: error) - } + return convert(task: task) } } diff --git a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift b/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift index fcb1375c..9c5d3f45 100644 --- a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift +++ b/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift @@ -54,4 +54,19 @@ public extension WorkerEmployCardVO { paymentType: .monthly, paymentAmount: "12,500" ) + + static let `default` = WorkerEmployCardVO( + dayLeft: 0, + isBeginnerPossible: true, + title: "기본값", + timeTakenForWalk: "기본값", + targetAge: 10, + targetLevel: 1, + targetGender: .notDetermined, + days: [], + startTime: "00:00", + endTime: "00:00", + paymentType: .hourly, + paymentAmount: "12,500" + ) } diff --git a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift index 0ceb9f34..0e6f0ab8 100644 --- a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift @@ -12,11 +12,9 @@ import Entity public protocol RecruitmentPostRepository: RepositoryBase { // MARK: Center - func registerPost( - input1: WorkTimeAndPayStateObject, - input2: AddressInputStateObject, - input3: CustomerInformationStateObject, - input4: CustomerRequirementStateObject, - input5: ApplicationDetailStateObject - ) -> Single + func registerPost(bundle: RegisterRecruitmentPostBundle) -> Single + + func getPostDetailForCenter(id: String) -> Single + + func editPostDetail(id: String, bundle: RegisterRecruitmentPostBundle) -> Single } diff --git a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift index 9a9d2ecb..737b1a79 100644 --- a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift @@ -11,6 +11,12 @@ import RxSwift public protocol RecruitmentPostUseCase: UseCaseBase { - /// 센터측이 공고를 등록하는 API입니다. + /// 센터측이 공고를 등록하는 액션입니다. func registerRecruitmentPost(inputs: RegisterRecruitmentPostBundle) -> Single> + + /// 센터측이 공고를 수정하는 액션입니다. + func editRecruitmentPost(id: String, inputs: RegisterRecruitmentPostBundle) -> Single> + + /// 센터측이 공고를 조회하는 액션입니다. + func getPostDetailForCenter(id: String) -> Single> } diff --git a/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift b/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift index df5bdba5..8f644b70 100644 --- a/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift +++ b/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift @@ -14,20 +14,20 @@ public protocol UseCaseBase: AnyObject { } public extension UseCaseBase { /// Repository로 부터 전달받은 언어레벨의 에러를 도메인 특화 에러로 변경하고, error를 Result의 Failure로, 성공을 Success로 변경합니다. - func convert(task: Single, errorClosure: @escaping (Error) -> F) -> Single> { + func convert(task: Single) -> Single> { Single.create { single in let disposable = task .subscribe { success in single(.success(.success(success))) } onFailure: { error in - single(.success(.failure(errorClosure(error)))) + single(.success(.failure(self.toDomainError(error: error)))) } return Disposables.create { disposable.dispose() } } } // MARK: InputValidationError - func toDomainError(error: Error) -> T { + private func toDomainError(error: Error) -> T { if let httpError = error as? HTTPResponseException { diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/ApplicationDetailViewContentView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/ApplicationDetailViewContentView.swift index f171579e..8d4cb47a 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/ApplicationDetailViewContentView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/ApplicationDetailViewContentView.swift @@ -209,12 +209,6 @@ public class ApplicationDetailViewContentView: UIView { } } - // 지원 방법 - stateFromVM.applyType.forEach { (key, value) in - - - } - applyTypeButtons .enumerated() .forEach { (index, button) in diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerInformationContentView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerInformationContentView.swift index 96df1526..cff54e8b 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerInformationContentView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerInformationContentView.swift @@ -246,14 +246,10 @@ public class CustomerInformationContentView: UIView { } // 출생년도 - if !stateFromVM.birthYear.isEmpty { - birthYearField.textString = stateFromVM.birthYear - } + birthYearField.textString = stateFromVM.birthYear // 몸무게 - if !stateFromVM.weight.isEmpty { - weightField.textField.textString = stateFromVM.weight - } + weightField.textField.textString = stateFromVM.weight.emptyDefault("-") // 요양등급 if let state = stateFromVM.careGrade { diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/ApplicationDetailDisplayingView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/ApplicationDetailDisplayingView.swift index 7c8d7517..19811692 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/ApplicationDetailDisplayingView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/ApplicationDetailDisplayingView.swift @@ -13,6 +13,11 @@ import RxSwift import Entity import DSKit +public protocol ApplicationDetailDisplayingVMable { + + var casting_applicationDetail: Driver { get } +} + public class ApplicationDetailDisplayingView: HStack { // Init @@ -101,7 +106,7 @@ public class ApplicationDetailDisplayingView: HStack { public extension ApplicationDetailDisplayingView { /// ViewModelType: ApplicationDetailContentVMable - func bind(viewModel: ApplicationDetailContentVMable) { + func bind(viewModel: ApplicationDetailDisplayingVMable) { viewModel .casting_applicationDetail diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/CustomerInformationDisplayingView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/CustomerInformationDisplayingView.swift index 2ab9690d..64c86890 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/CustomerInformationDisplayingView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/CustomerInformationDisplayingView.swift @@ -13,16 +13,11 @@ import RxSwift import Entity import DSKit -public struct KeyValueListViewComponent { - public let title: String - public let valueLabelView: IdleLabel - public let subValue: String? + +public protocol CustomerInformationDisplayingVMable { - public init(title: String, valueLabelView: IdleLabel, subValue: String? = nil) { - self.title = title - self.valueLabelView = valueLabelView - self.subValue = subValue - } + var casting_customerInformation: Driver { get } + var casting_customerRequirement: Driver { get } } public class CustomerInformationDisplayingView: VStack { @@ -207,7 +202,7 @@ public class CustomerInformationDisplayingView: VStack { public extension CustomerInformationDisplayingView { - func bind(viewModel: CustomerInformationContentVMable & CustomerRequirementContentVMable) { + func bind(viewModel: CustomerInformationDisplayingVMable) { viewModel .casting_customerInformation @@ -256,3 +251,15 @@ public extension CustomerInformationDisplayingView { .disposed(by: disposeBag) } } + +struct KeyValueListViewComponent { + public let title: String + public let valueLabelView: IdleLabel + public let subValue: String? + + public init(title: String, valueLabelView: IdleLabel, subValue: String? = nil) { + self.title = title + self.valueLabelView = valueLabelView + self.subValue = subValue + } +} diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/WorkConditionDisplayingView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/WorkConditionDisplayingView.swift index 671a57c7..5569d4cc 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/WorkConditionDisplayingView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/WorkConditionDisplayingView.swift @@ -13,6 +13,12 @@ import RxSwift import Entity import DSKit +public protocol WorkConditionDisplayingVMable { + + var casting_workTimeAndPay: Driver { get } + var casting_addressInput: Driver { get } +} + public class WorkConditionDisplayingView: HStack { // Init @@ -102,7 +108,7 @@ public class WorkConditionDisplayingView: HStack { public extension WorkConditionDisplayingView { - func bind(viewModel: WorkTimeAndPayContentVMable & AddressInputViewContentVMable) { + func bind(viewModel: WorkConditionDisplayingVMable) { viewModel .casting_workTimeAndPay diff --git a/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift index b37cfb62..8c9591cd 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift @@ -10,5 +10,5 @@ import RxCocoa public protocol DefaultAlertOutputable { - var alert: Driver { get } + var alert: Driver? { get } } diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index 76918688..a4ea3d36 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -23,7 +23,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let store = TestStore() try! store.saveAuthToken( - accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMjIyNzcxMywibmJmIjoxNzIyMjI3NzEzLCJleHAiOjE3MjIyMjgzMTMsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkwZmNjNS01OGI1LTdlOWYtYTE3NS1hZDUwMjZjMzI4M2EiLCJwaG9uZU51bWJlciI6IjAxMC00NDQ0LTUyMzIiLCJ1c2VyVHlwZSI6ImNlbnRlciJ9.gJXEtDruIRqYM9R6aszejnIDOm8VP6ROnrNqESIdssE", + accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMzYyMDAzNywibmJmIjoxNzIzNjIwMDM3LCJleHAiOjE3MjM2MjA2MzcsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkxNGZjMi04YTk4LTdhNDAtYWFmYS04OWM0MDhiZmEyOGMiLCJwaG9uZU51bWJlciI6IjAxMC00NDQ0LTUyMzIiLCJ1c2VyVHlwZSI6ImNlbnRlciJ9.cYk9E0EJwMpX3wxhQq6R5nMKaVGj2yA7csDybB-Jn8o", refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMjIyNzcxMywibmJmIjoxNzIyMjI3NzEzLCJleHAiOjE3MjM0MzczMTMsInR5cGUiOiJSRUZSRVNIX1RPS0VOIiwidXNlcklkIjoiMDE5MGZjYzUtNThiNS03ZTlmLWExNzUtYWQ1MDI2YzMyODNhIiwidXNlclR5cGUiOiJjZW50ZXIifQ.EtV-qojoAl-H7VVm-Dr2tYf6Hkbx3OdwbsxduAOFf6I" ) @@ -34,17 +34,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let navigationController = UINavigationController() navigationController.setNavigationBarHidden(true, animated: false) - let vm = RegisterRecruitmentPostVM( - recruitmentPostUseCase: DefaultRecruitmentPostUseCase( - repository: DefaultRecruitmentPostRepository() - ) - ) - let coordinator = RegisterRecruitmentCoordinator( - viewModel: vm, - navigationController: navigationController - ) +// let coordinator = RegisterRecruitmentCoordinator( +// viewModel: vm, +// navigationController: navigationController +// ) +// +// let vm = RegisterRecruitmentPostVM( +// registerRecruitmentPostCoordinator: coordinator, recruitmentPostUseCase: DefaultRecruitmentPostUseCase( +// repository: DefaultRecruitmentPostRepository() +// ) +// ) +// let vc = CenterRecruitmentPostBoardVC() // vc.bind(viewModel: vm) diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/EditPostCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/EditPostCoordinator.swift new file mode 100644 index 00000000..c9ffb3fa --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/EditPostCoordinator.swift @@ -0,0 +1,55 @@ +// +// EditPostCoordinator.swift +// CenterFeature +// +// Created by choijunios on 8/14/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class EditPostCoordinator: ChildCoordinator { + + public struct Dependency { + let navigationController: UINavigationController + let viewModel: EditPostViewModelable + + public init(navigationController: UINavigationController, viewModel: EditPostViewModelable) { + self.navigationController = navigationController + self.viewModel = viewModel + } + } + + public weak var viewControllerRef: UIViewController? + public weak var parent: ParentCoordinator? + + public let navigationController: UINavigationController + public let viewModel: EditPostViewModelable + + public init( + dependency: Dependency + ) { + self.navigationController = dependency.navigationController + self.viewModel = dependency.viewModel + viewModel.editPostCoordinator = self + } + + deinit { + printIfDebug("\(String(describing: EditPostCoordinator.self))") + } + + public func start() { + let vc = EditPostVC() + vc.bind(viewModel: viewModel) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } +} + diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostDetailForCenterCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostDetailForCenterCoordinator.swift new file mode 100644 index 00000000..848b7c2c --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostDetailForCenterCoordinator.swift @@ -0,0 +1,77 @@ +// +// PostDetailForCenterCoordinator.swift +// CenterFeature +// +// Created by choijunios on 8/14/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class PostDetailForCenterCoordinator: ChildCoordinator { + + public struct Dependency { + let postId: String + var applicantCount: Int? + let navigationController: UINavigationController + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(postId: String, applicantCount: Int? = nil, navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.postId = postId + self.applicantCount = applicantCount + self.navigationController = navigationController + self.recruitmentPostUseCase = recruitmentPostUseCase + } + } + + public weak var viewControllerRef: UIViewController? + public weak var parent: RecruitmentManagementCoordinatable? + + public let navigationController: UINavigationController + let postId: String + var applicantCount: Int? + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init( + dependency: Dependency + ) { + self.navigationController = dependency.navigationController + self.postId = dependency.postId + self.recruitmentPostUseCase = dependency.recruitmentPostUseCase + self.applicantCount = dependency.applicantCount + } + + deinit { + printIfDebug("\(String(describing: PostDetailForCenterCoordinator.self))") + } + + public func start() { + let vc = PostDetailForCenterVC() + let vm = PostDetailForCenterVM( + id: postId, + applicantCount: applicantCount, + coordinator: self, + recruitmentPostUseCase: recruitmentPostUseCase + ) + viewControllerRef = vc + vc.bind(viewModel: vm) + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } +} + +extension PostDetailForCenterCoordinator { + + func showPostEditScreen(postId: String) { + parent?.showEditScreen(postId: postId) + } + func showCheckApplicantScreen(postId: String) { + parent?.showCheckingApplicantScreen(postId: postId) + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostOverviewCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostOverviewCoordinator.swift index 997e1210..b1403340 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostOverviewCoordinator.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostOverviewCoordinator.swift @@ -12,19 +12,26 @@ import Entity public class PostOverviewCoordinator: ChildCoordinator { + public struct Dependency { + let navigationController: UINavigationController + let viewModel: PostOverviewViewModelable + + public init(navigationController: UINavigationController, viewModel: PostOverviewViewModelable) { + self.navigationController = navigationController + self.viewModel = viewModel + } + } + public weak var viewControllerRef: UIViewController? public weak var parent: RegisterRecruitmentPostCoordinatable? public let navigationController: UINavigationController - public let viewModel: RegisterRecruitmentPostViewModelable + public let viewModel: PostOverviewViewModelable - public init( - viewModel: RegisterRecruitmentPostViewModelable, - navigationController: UINavigationController - ) { - self.viewModel = viewModel - self.navigationController = navigationController + public init(dependency: Dependency) { + self.viewModel = dependency.viewModel + self.navigationController = dependency.navigationController } deinit { @@ -33,25 +40,20 @@ public class PostOverviewCoordinator: ChildCoordinator { public func start() { let vc = PostOverviewVC() - vc.coordinator = self vc.bind(viewModel: viewModel) + viewModel.postOverviewCoordinator = self viewControllerRef = vc navigationController.pushViewController(vc, animated: true) } public func coordinatorDidFinish() { + popViewController() parent?.removeChildCoordinator(self) } } extension PostOverviewCoordinator { - func showCompleteScreen() { parent?.showRegisterCompleteScreen() } - - func backToEditScreen() { - popViewController() - coordinatorDidFinish() - } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/RegisterRecruitmentCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/RegisterRecruitmentCoordinator.swift index 57a6a991..93c60b38 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/RegisterRecruitmentCoordinator.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/RegisterRecruitmentCoordinator.swift @@ -35,7 +35,6 @@ public class RegisterRecruitmentCoordinator: ChildCoordinator { public func start() { let vc = RegisterRecruitmentPostVC() vc.bind(viewModel: viewModel) - vc.coordinator = self viewControllerRef = vc navigationController.pushViewController(vc, animated: true) } 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 a280b591..0497c0f5 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -453,7 +453,7 @@ public class CenterProfileViewController: BaseViewController { } output - .alert + .alert? .drive { [weak self] vo in self?.showAlert(vo: vo) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift index 1587474d..b92b49d6 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift @@ -117,7 +117,25 @@ class CenterEmployCardVM: CenterEmployCardViewModelable { // MARK: 버튼 처리 checkApplicantBtnClicked .subscribe(onNext: { [weak self] _ in - self?.coordinator?.showCheckingApplicantScreen(vo) + guard let self else { return } + + coordinator?.showCheckingApplicantScreen(postId: id) + }) + .disposed(by: disposeBag) + + cardClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + coordinator?.showPostDetailScreenForCenter(postId: id, applicantCount: vo.applicantCount) + }) + .disposed(by: disposeBag) + + editPostBtnClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + coordinator?.showEditScreen(postId: id) }) .disposed(by: disposeBag) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift new file mode 100644 index 00000000..fc5ce1aa --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift @@ -0,0 +1,300 @@ +// +// PostDetailForCenterVC.swift +// CenterFeature +// +// Created by choijunios on 8/14/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class PostDetailForCenterVC: BaseViewController { + + // Init + + // Not init + var viewModel: PostDetailViewModelable? + + // View + lazy var navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar(titleText: "공고 상세 정보", innerViews: [postEditButton]) + return bar + }() + let postEditButton: TextButtonType2 = { + let button = TextButtonType2(labelText: "공고 수정하기") + button.label.typography = .Body3 + button.label.attrTextColor = DSKitAsset.Colors.gray300.color + button.layoutMargins = .init(top: 5.5, left:12, bottom: 5.5, right: 12) + button.layer.cornerRadius = 16 + return button + }() + + let sampleCard: WorkerEmployCard = { + let card = WorkerEmployCard() + card.starButton.isUserInteractionEnabled = false + return card + }() + + let screenFoWorkerButton: TextImageButtonType2 = { + let button = TextImageButtonType2() + button.textLabel.textString = "보호사 측 화면으로 보기 " + button.textLabel.attrTextColor = DSKitAsset.Colors.gray300.color + button.imageView.image = DSKitAsset.Icons.chevronRight.image + button.imageView.tintColor = DSKitAsset.Colors.gray300.color + button.layoutMargins = .zero + button.layer.borderWidth = 0.0 + return button + }() + + let checkApplicantButton: IdlePrimaryButton = { + let button = IdlePrimaryButton(level: .large) + button.label.textString = "" + return button + }() + + // Overviews + let workConditionOV = WorkConditionDisplayingView() + let customerInfoOV = CustomerInformationDisplayingView() + let applyInfoOverView = ApplicationDetailDisplayingView() + + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + setAppearance() + setLayout() + setObservable() + } + + private func setAppearance() { + view.backgroundColor = .white + } + + private func setLayout() { + + let scrollView = UIScrollView() + scrollView.delaysContentTouches = false + let contentGuide = scrollView.contentLayoutGuide + let frameGuide = scrollView.frameLayoutGuide + + let contentView = UIView() + contentView.layoutMargins = .init(top: 24, left: 20, bottom: 16, right: 20) + + scrollView.addSubview(contentView) + contentView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + + contentView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor), + + contentView.topAnchor.constraint(equalTo: contentGuide.topAnchor), + contentView.leftAnchor.constraint(equalTo: contentGuide.leftAnchor), + contentView.rightAnchor.constraint(equalTo: contentGuide.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor), + ]) + + // Overview + + let overviewData: [(title: String, view: UIView)] = [ + ("근무 조건", workConditionOV), + ("고객 정보", customerInfoOV), + ("추가 지원 정보", applyInfoOverView), + ] + + let overViews = overviewData.map { (title, view) in + + let partView = UIView() + partView.backgroundColor = .white + partView.layoutMargins = .init(top: 24, left: 20, bottom: 24, right: 20) + + let titleLabel = IdleLabel(typography: .Subtitle1) + titleLabel.textString = title + + [ + titleLabel, + view + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + partView.addSubview($0) + } + + NSLayoutConstraint.activate([ + + titleLabel.topAnchor.constraint(equalTo: partView.layoutMarginsGuide.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: partView.layoutMarginsGuide.leftAnchor), + + view.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20.43), + view.leftAnchor.constraint(equalTo: partView.layoutMarginsGuide.leftAnchor), + view.rightAnchor.constraint(equalTo: partView.layoutMarginsGuide.rightAnchor), + view.bottomAnchor.constraint(equalTo: partView.layoutMarginsGuide.bottomAnchor), + ]) + + return partView + } + + let overViewContentView = VStack( + [ + overViews.map({ view in + [ + Spacer(height: 8), + view + ] + }).flatMap { $0 } + ].flatMap { $0 }, + alignment: .fill + ) + overViewContentView.backgroundColor = DSKitAsset.Colors.gray050.color + + // 확인했어요 + let canEditRemiderLabel: IdleLabel = .init(typography: .Body3) + canEditRemiderLabel.attrTextColor = DSKitAsset.Colors.gray300.color + canEditRemiderLabel.textString = "공고 등록 후에도 공고 내용을 수정할 수 있어요. " + + // 카드 모양 조정 + let cardBackgroundView = UIView() + cardBackgroundView.layer.borderWidth = 1 + cardBackgroundView.layer.cornerRadius = 12 + cardBackgroundView.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor + cardBackgroundView.layoutMargins = .init( + top: 16, + left: 16, + bottom: 16, + right: 16 + ) + cardBackgroundView.addSubview(sampleCard) + sampleCard.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + sampleCard.topAnchor.constraint(equalTo: cardBackgroundView.layoutMarginsGuide.topAnchor), + sampleCard.leftAnchor.constraint(equalTo: cardBackgroundView.layoutMarginsGuide.leftAnchor), + sampleCard.rightAnchor.constraint(equalTo: cardBackgroundView.layoutMarginsGuide.rightAnchor), + sampleCard.bottomAnchor.constraint(equalTo: cardBackgroundView.layoutMarginsGuide.bottomAnchor), + ]) + + // scroll view + [ + cardBackgroundView, + screenFoWorkerButton, + overViewContentView, + canEditRemiderLabel, + checkApplicantButton + + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview($0) + } + + NSLayoutConstraint.activate([ + + cardBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor), + cardBackgroundView.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), + cardBackgroundView.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), + + screenFoWorkerButton.topAnchor.constraint(equalTo: cardBackgroundView.bottomAnchor, constant: 12), + screenFoWorkerButton.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), + + overViewContentView.topAnchor.constraint(equalTo: screenFoWorkerButton.bottomAnchor, constant: 24), + overViewContentView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + overViewContentView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + + canEditRemiderLabel.topAnchor.constraint(equalTo: overViewContentView.bottomAnchor, constant: 32), + canEditRemiderLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), + + checkApplicantButton.topAnchor.constraint(equalTo: canEditRemiderLabel.bottomAnchor, constant: 12), + checkApplicantButton.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), + checkApplicantButton.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), + checkApplicantButton.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), + ]) + + // main view + [ + navigationBar, + scrollView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor), + navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor), + + scrollView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + ]) + + } + + private func setObservable() { + + } + + public func bind(viewModel: PostDetailViewModelable) { + + self.viewModel = viewModel + + // Input + // 지원공고자 확인 버튼 클릭 + checkApplicantButton + .rx.tap + .bind(to: viewModel.checkApplicationButtonClicked) + .disposed(by: disposeBag) + + // 나가기 + navigationBar.backButton.rx.tap + .bind(to: viewModel.exitButtonClicked) + .disposed(by: disposeBag) + + // 수정화면으로 이동 + postEditButton.eventPublisher + .bind(to: viewModel.postEditButtonClicked) + .disposed(by: disposeBag) + + // 화면이 등장할 때마다 유효한 상태를 불러옵니다. + self.rx.viewWillAppear + .map({ _ in }) + .bind(to: viewModel.viewWillAppear) + .disposed(by: disposeBag) + + // Ouptut + + checkApplicantButton.label.textString = "지원자 \(viewModel.applicantCount ?? 0)명 조회" + + workConditionOV.bind(viewModel: viewModel) + customerInfoOV.bind(viewModel: viewModel) + applyInfoOverView.bind(viewModel: viewModel) + + viewModel + .requestDetailFailure? + .drive(onNext: { [weak self] alertVO in + + self?.showAlert(vo: alertVO, onClose: { + self?.viewModel?.exitButtonClicked.accept(()) + }) + }) + .disposed(by: disposeBag) + + viewModel + .workerEmployCardVO? + .drive(onNext: { [sampleCard] vo in + sampleCard.bind(vo: vo) + }) + .disposed(by: disposeBag) + } +} + diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift index eb81b020..b92f47d1 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift @@ -13,8 +13,33 @@ import RxSwift import Entity import DSKit -public protocol WorkerCardViewModelable { - var workerEmployCardVO: Driver { get } +public typealias PostDetailDisplayingViewModelable = ApplicationDetailDisplayingVMable & CustomerInformationDisplayingVMable & WorkConditionDisplayingVMable + +public protocol PostOverviewViewModelable: + AnyObject, + ApplicationDetailContentVMable, + CustomerInformationContentVMable, + CustomerRequirementContentVMable, + WorkTimeAndPayContentVMable, + AddressInputViewContentVMable, + DefaultAlertOutputable, + PostDetailDisplayingViewModelable +{ + + var postOverviewCoordinator: PostOverviewCoordinator? { get set } + + /// 공고등록에 성공한 경우 해당 이벤트를 전달 받습니다 + var workerEmployCardVO: Driver? { get } + + /// 유효한 값을 가져옵니다. + func fetchFromState() + /// 수정중인 값을 API를 사용하여 전송할 값(State)에 반영합니다. + func updateToState() + + var postEditButtonClicked: PublishRelay { get } + var overViewExitButtonClicked: PublishRelay { get } + var registerButtonClicked: PublishRelay { get } + var overViewWillAppear: PublishRelay { get } } public class PostOverviewVC: BaseViewController { @@ -22,9 +47,7 @@ public class PostOverviewVC: BaseViewController { // Init // Not init - weak var coordinator: PostOverviewCoordinator? - - var viewModel: RegisterRecruitmentPostViewModelable? + var viewModel: PostOverviewViewModelable? // View let backButton: UIButton = { @@ -274,78 +297,57 @@ public class PostOverviewVC: BaseViewController { } private func setObservable() { - backButton.rx.tap - .subscribe(onNext: { [coordinator] _ in - - coordinator?.backToEditScreen() - }) - .disposed(by: disposeBag) - - postEditButton.eventPublisher - .subscribe(onNext: { [weak self] _ in - - guard let self, let vm = viewModel else { return } - - let vc = EditPostVC() - vc.bind(viewModel: vm) - self.navigationController?.pushViewController( - vc, - animated: true) - }) + executeRegisterButton + .rx.tap + .subscribe { [weak self] _ in + self?.view.isUserInteractionEnabled = false + } .disposed(by: disposeBag) } - public func bind(viewModel: RegisterRecruitmentPostViewModelable) { + func bind(viewModel: PostOverviewViewModelable) { self.viewModel = viewModel + // 앞전까지 입력한 정보를 저장합니다. + viewModel.updateToState() + // 공고등록 요청 executeRegisterButton .rx.tap - .subscribe { [weak self] _ in - - guard let self else { return } - - // 공고 등록중 인터렉션 비활성화 - view.isUserInteractionEnabled = false - viewModel.requestRegisterPost() - } + .bind(to: viewModel.registerButtonClicked) .disposed(by: disposeBag) + // 나가기 + backButton.rx.tap + .bind(to: viewModel.overViewExitButtonClicked) + .disposed(by: disposeBag) - // 앞전까지 입력한 정보를 저장합니다. - viewModel.updateToState() + // 수정화면으로 이동 + postEditButton.eventPublisher + .bind(to: viewModel.postEditButtonClicked) + .disposed(by: disposeBag) // 화면이 등장할 때마다 유효한 상태를 불러옵니다. self.rx.viewWillAppear - .subscribe { [viewModel, sampleCard] _ in - viewModel.fetchFromState() - - // 예시카드 바인딩 - let disposable = viewModel - .workerEmployCardVO - .drive(onNext: { [sampleCard] vo in - sampleCard.bind(vo: vo) - }) - disposable.dispose() - } + .map({ _ in }) + .bind(to: viewModel.overViewWillAppear) .disposed(by: disposeBag) + // Ouptut workConditionOV.bind(viewModel: viewModel) customerInfoOV.bind(viewModel: viewModel) applyInfoOverView.bind(viewModel: viewModel) - // Ouptut - // 공고등록성공 수신 viewModel - .postRegistrationSuccess - .drive(onNext: { [coordinator] _ in - coordinator?.showCompleteScreen() + .workerEmployCardVO? + .drive(onNext: { [sampleCard] vo in + sampleCard.bind(vo: vo) }) .disposed(by: disposeBag) - + viewModel - .alert + .alert? .drive(onNext: { [weak self] vo in guard let self else { return } view.isUserInteractionEnabled = true diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/EditPost/EditPostVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/EditPost/EditPostVC.swift index 2b48ee89..35772326 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/EditPost/EditPostVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/EditPost/EditPostVC.swift @@ -13,10 +13,17 @@ import RxSwift import Entity import DSKit -public protocol CheckAllPostInputValidation { - - var requestAllInputValidation: PublishRelay { get } - var validationResultWithAlert: Driver? { get } +public protocol EditPostViewModelable: AnyObject, + ApplicationDetailContentVMable, + CustomerInformationContentVMable, + CustomerRequirementContentVMable, + WorkTimeAndPayContentVMable, + AddressInputViewContentVMable +{ + var editPostCoordinator: EditPostCoordinator? { get set } + var editViewExitButtonClicked: PublishRelay { get } + var saveButtonClicked: PublishRelay { get } + var requestSaveFailure: Driver? { get } } public class EditPostVC: BaseViewController { @@ -263,40 +270,27 @@ public class EditPostVC: BaseViewController { private func setObservable() { - navigationBar - .eventPublisher - .subscribe(onNext: { [weak self] _ in - self?.navigationController?.popViewController(animated: true) - }) - .disposed(by: disposeBag) + } - public func bind(viewModel: RegisterRecruitmentPostViewModelable) { + public func bind(viewModel: EditPostViewModelable) { editingCompleteButton .eventPublisher - .subscribe { [weak self, viewModel] _ in - - guard let self else { return } + .bind(to: viewModel.saveButtonClicked) + .disposed(by: disposeBag) + + navigationBar + .eventPublisher + .bind(to: viewModel.editViewExitButtonClicked) + .disposed(by: disposeBag) + + viewModel + .requestSaveFailure? + .drive(onNext: { [weak self] alertVO in - viewModel - .allInputsValid() - .subscribe (onSuccess: { [weak self, viewModel] vo in - - guard let self else { return } - - if let alertVO = vo { - - showAlert(vo: alertVO) - - } else { - /// 저장하기 버튼을 누른 경우 값을 업데이트 합니다. - viewModel.updateToState() - navigationController?.popViewController(animated: true) - } - }) - .disposed(by: disposeBag) - } + self?.showAlert(vo: alertVO) + }) .disposed(by: disposeBag) workTimeAndPaymentEditView.bind(viewModel: viewModel) diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterRecruitmentPostVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterRecruitmentPostVC.swift index 694d7298..cbf814a9 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterRecruitmentPostVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterRecruitmentPostVC.swift @@ -25,25 +25,16 @@ public protocol RegisterRecruitmentPostVMBindable { func bind(viewModel: RegisterRecruitmentPostViewModelable) } -public protocol RegisterRecruitmentPostViewModelable: - ApplicationDetailContentVMable, - CustomerInformationContentVMable, - CustomerRequirementContentVMable, - WorkTimeAndPayContentVMable, - AddressInputViewContentVMable, - WorkerCardViewModelable, - DefaultAlertOutputable +public protocol RegisterRecruitmentPostViewModelable: + + // 수정화면 요구사항 + EditPostViewModelable, + + // 오버뷰 화면 요구 사항 + PostOverviewViewModelable { - /// 현재 유효한 값으로 공고를 등록할 것을 요청합니다. - func requestRegisterPost() - /// 공고등록에 성공한 경우 해당 이벤트를 전달 받습니다 - var postRegistrationSuccess: Driver { get } - /// 유효한 값을 가져옵니다. - func fetchFromState() - /// 수정중인 값을 API를 사용하여 전송할 값(State)에 반영합니다. - func updateToState() - /// 모든 입력이 유효한지 확인하고 유효하지 않은 첫번째 섹션의 Alert정보를 반환합니다. - func allInputsValid() -> Single + /// 코디네이터 + var registerRecruitmentPostCoordinator: RegisterRecruitmentPostCoordinatable? { get } } public class RegisterRecruitmentPostVC: BaseViewController { @@ -51,7 +42,6 @@ public class RegisterRecruitmentPostVC: BaseViewController { // Init // Not Init - weak var coordinator: RegisterRecruitmentCoordinator? /// 현재 스크린의 넓이를 의미합니다. private var screenWidth: CGFloat { @@ -226,7 +216,7 @@ public class RegisterRecruitmentPostVC: BaseViewController { } else { // 오버뷰화면으로 이동 - coordinator?.showOverViewScreen() + viewModel?.registerRecruitmentPostCoordinator?.showOverViewScreen() } } @@ -249,7 +239,7 @@ public class RegisterRecruitmentPostVC: BaseViewController { } else { // 돌아가기, Coordinator호출 - coordinator?.registerFinished() + viewModel?.registerRecruitmentPostCoordinator?.registerFinished() } } 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 c49e5d44..992b92b1 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -269,7 +269,7 @@ public extension CenterProfileViewModel { // 요구사항 X public var editingValidation: Driver - public var alert: Driver + public var alert: Driver? init(centerName: Driver, centerLocation: Driver, centerPhoneNumber: Driver, centerIntroduction: Driver, displayingImage: Driver, isEditingMode: Driver, editingValidation: Driver, alert: Driver) { self.centerName = centerName diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/EditPostVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/EditPostVM.swift new file mode 100644 index 00000000..189fdcf1 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/EditPostVM.swift @@ -0,0 +1,478 @@ +// +// EditPostVM.swift +// CenterFeature +// +// Created by choijunios on 8/14/24. +// + +import Foundation +import RxSwift +import RxCocoa +import Entity +import PresentationCore +import UseCaseInterface + + +public class EditPostVM: EditPostViewModelable { + + // Init + let id: String + let recruitmentPostUseCase: RecruitmentPostUseCase + + // MARK: Edit Screen + public var editPostViewWillAppear: RxRelay.PublishRelay = .init() + public weak var editPostCoordinator: EditPostCoordinator? + public var editViewExitButtonClicked: RxRelay.PublishRelay = .init() + public var saveButtonClicked: RxRelay.PublishRelay = .init() + public var requestSaveFailure: RxCocoa.Driver? + + // MARK: State + var state_workTimeAndPay: WorkTimeAndPayStateObject = .init() + var state_customerRequirement: CustomerRequirementStateObject = .init() + var state_customerInformation: CustomerInformationStateObject = .init() + var state_applicationDetail: ApplicationDetailStateObject = .init() + var state_addressInfo: AddressInputStateObject = .init() + + // MARK: Editing + private let editing_workTimeAndPay: BehaviorRelay = .init(value: .init()) + private let editing_customerRequirement: BehaviorRelay = .init(value: .init()) + private let editing_customerInformation: BehaviorRelay = .init(value: .init()) + private let editing_applicationDetail: BehaviorRelay = .init(value: .init()) + private let editing_addressInfo: BehaviorRelay = .init(value: .init()) + + // MARK: Casting + public var casting_addressInput: Driver + public var casting_workTimeAndPay: Driver + public var casting_customerRequirement: Driver + public var casting_customerInformation: Driver + public var casting_applicationDetail: Driver + + // MARK: Address input + public var addressInformation: PublishRelay = .init() + + // MARK: Work time and pay + public var selectedDay: PublishRelay<(WorkDay, Bool)> = .init() + public var workStartTime: PublishRelay = .init() + public var workEndTime: PublishRelay = .init() + public var paymentType: PublishRelay = .init() + public var paymentAmount: PublishRelay = .init() + + + // MARK: Customer requirement + public var mealSupportNeeded: PublishRelay = .init() + public var toiletSupportNeeded: PublishRelay = .init() + public var movingSupportNeeded: PublishRelay = .init() + public var dailySupportTypes: PublishRelay<(DailySupportType, Bool)> = .init() + public var additionalRequirement: PublishRelay = .init() + + + + // MARK: Customer information + public var name: PublishRelay = .init() + public var gender: PublishRelay = .init() + public var birthYear: PublishRelay = .init() + public var weight: PublishRelay = .init() + public var careGrade: PublishRelay = .init() + public var cognitionState: PublishRelay = .init() + public var deceaseDescription: PublishRelay = .init() + + + + + // MARK: Application detail + public var experiencePreferenceType: PublishRelay = .init() + public var applyType: PublishRelay<(Entity.ApplyType, Bool)> = .init() + public var applyDeadlineType: PublishRelay = .init() + public var deadlineDate: BehaviorRelay = .init(value: nil) + + public var deadlineString: Driver + + + public var addressInputNextable: Driver + public var workTimeAndPayNextable: Driver + public var customerRequirementNextable: Driver + public var customerInformationNextable: Driver + public var applicationDetailViewNextable: Driver + + + // MARK: 모든 섹션의 유효성 확인 + private let validationStateQueue = DispatchQueue.global(qos: .userInteractive) + private var validationState: [RegisterRecruitmentPostInputSection: Bool] = { + var dict: [RegisterRecruitmentPostInputSection: Bool] = [:] + RegisterRecruitmentPostInputSection.allCases.forEach { section in + dict[section] = false + } + return dict + }() + + // MARK: Alert + public var alert: Driver? + + // 옵셔널한 입력을 유지합니다. + let disposeBag = DisposeBag() + + public init( + id: String, + recruitmentPostUseCase: RecruitmentPostUseCase + ) { + self.id = id + self.recruitmentPostUseCase = recruitmentPostUseCase + + // MARK: Work time and pay + casting_workTimeAndPay = editing_workTimeAndPay.asDriver(onErrorJustReturn: .mock) + + selectedDay + .subscribe { [editing_workTimeAndPay] (day, isActive) in + editing_workTimeAndPay.value.selectedDays[day] = isActive + } + .disposed(by: disposeBag) + + workStartTime + .subscribe { [editing_workTimeAndPay] newValue in + editing_workTimeAndPay.value.workStartTime = newValue + } + .disposed(by: disposeBag) + + workEndTime + .subscribe { [editing_workTimeAndPay] newValue in + editing_workTimeAndPay.value.workEndTime = newValue + } + .disposed(by: disposeBag) + + paymentType + .subscribe { [editing_workTimeAndPay] newValue in + editing_workTimeAndPay.value.paymentType = newValue + } + .disposed(by: disposeBag) + + paymentAmount + .subscribe { [editing_workTimeAndPay] newValue in + editing_workTimeAndPay.value.paymentAmount = newValue + } + .disposed(by: disposeBag) + + let workTimeAndPayInputValidation = saveButtonClicked + .map { [editing_workTimeAndPay] _ in + let object = editing_workTimeAndPay.value + + let activeDayCnt = object.selectedDays.keys.reduce(0) { partialResult, key in + partialResult + (object.selectedDays[key] == true ? 1 : 0) + } + + return activeDayCnt > 0 && + object.workStartTime != nil && + object.workEndTime != nil && + object.paymentType != nil && + !object.paymentAmount.isEmpty + } + + workTimeAndPayNextable = workTimeAndPayInputValidation.asDriver(onErrorJustReturn: false) + + + // MARK: Address input + casting_addressInput = editing_addressInfo.asDriver(onErrorJustReturn: .mock) + + addressInformation + .subscribe { [editing_addressInfo] newValue in + editing_addressInfo.value.addressInfo = newValue + } + .disposed(by: disposeBag) + + let addressInputValidation = saveButtonClicked + .map { [editing_addressInfo] _ in + let object = editing_addressInfo.value + + return object.addressInfo != nil + } + + addressInputNextable = addressInputValidation.asDriver(onErrorJustReturn: false) + + + // MARK: Customer requirement + casting_customerRequirement = editing_customerRequirement.asDriver(onErrorJustReturn: .mock) + + mealSupportNeeded + .subscribe { [editing_customerRequirement] newValue in + editing_customerRequirement.value.mealSupportNeeded = newValue + } + .disposed(by: disposeBag) + + toiletSupportNeeded + .subscribe { [editing_customerRequirement] newValue in + editing_customerRequirement.value.toiletSupportNeeded = newValue + } + .disposed(by: disposeBag) + + movingSupportNeeded + .subscribe { [editing_customerRequirement] newValue in + editing_customerRequirement.value.movingSupportNeeded = newValue + } + .disposed(by: disposeBag) + + // optional + dailySupportTypes + .subscribe { [editing_customerRequirement] (type, isAtive) in + editing_customerRequirement.value.dailySupportTypeNeeds[type] = isAtive + } + .disposed(by: disposeBag) + + additionalRequirement + .subscribe { [editing_customerRequirement] newValue in + editing_customerRequirement.value.additionalRequirement = newValue + } + .disposed(by: disposeBag) + + let customerRequirementInputValidation = saveButtonClicked + .map { [editing_customerRequirement] _ in + let requirement = editing_customerRequirement.value + + return requirement.mealSupportNeeded != nil && + requirement.toiletSupportNeeded != nil && + requirement.movingSupportNeeded != nil + } + + customerRequirementNextable = customerRequirementInputValidation.asDriver(onErrorJustReturn: false) + + // MARK: Customer information + casting_customerInformation = editing_customerInformation.asDriver(onErrorJustReturn: .mock) + + name + .subscribe { [editing_customerInformation] newValue in + editing_customerInformation.value.name = newValue + } + .disposed(by: disposeBag) + + gender + .subscribe { [editing_customerInformation] newValue in + editing_customerInformation.value.gender = newValue + } + .disposed(by: disposeBag) + + birthYear + .subscribe { [editing_customerInformation] newValue in + editing_customerInformation.value.birthYear = newValue + } + .disposed(by: disposeBag) + + careGrade + .subscribe { [editing_customerInformation] newValue in + editing_customerInformation.value.careGrade = newValue + } + .disposed(by: disposeBag) + + cognitionState + .subscribe { [editing_customerInformation] newValue in + editing_customerInformation.value.cognitionState = newValue + } + .disposed(by: disposeBag) + + // optional + weight + .subscribe { [editing_customerInformation] newValue in + editing_customerInformation.value.weight = newValue + } + .disposed(by: disposeBag) + + deceaseDescription + .subscribe { [editing_customerInformation] newValue in + editing_customerInformation.value.deceaseDescription = newValue + } + .disposed(by: disposeBag) + + let customerInformationInputValidation = saveButtonClicked + .map { [editing_customerInformation] _ in + let customerInfo = editing_customerInformation.value + + return !customerInfo.name.isEmpty && + !customerInfo.birthYear.isEmpty && + customerInfo.careGrade != nil && + customerInfo.cognitionState != nil + } + + customerInformationNextable = customerInformationInputValidation.asDriver(onErrorJustReturn: false) + + // MARK: Application detail + casting_applicationDetail = editing_applicationDetail.asDriver(onErrorJustReturn: .mock) + + experiencePreferenceType + .subscribe { [editing_applicationDetail] newValue in + editing_applicationDetail.value.experiencePreferenceType = newValue + } + .disposed(by: disposeBag) + + applyType + .subscribe { [editing_applicationDetail] (applyType, isActive) in + editing_applicationDetail.value.applyType[applyType] = isActive + } + .disposed(by: disposeBag) + + applyDeadlineType + .subscribe { [editing_applicationDetail] newValue in + editing_applicationDetail.value.applyDeadlineType = newValue + } + .disposed(by: disposeBag) + + deadlineDate + .subscribe { [editing_applicationDetail] newValue in + editing_applicationDetail.value.deadlineDate = newValue + } + .disposed(by: disposeBag) + + // optional + deadlineString = deadlineDate + .compactMap { $0 } + .map { $0.convertDateToString() } + .asDriver(onErrorJustReturn: "") + + let applicationDetailInputValidation = saveButtonClicked + .map { [editing_applicationDetail] _ in + + let state = editing_applicationDetail.value + + let activeApplyTypeCnt = state.applyType.reduce(0) { partialResult, keyValue in + partialResult + (keyValue.value ? 1 : 0) + } + + if state.applyDeadlineType != nil, + activeApplyTypeCnt != 0, + state.experiencePreferenceType != nil { + + if state.applyDeadlineType == .specificDate { + + return state.deadlineDate != nil + } + return true + } + return false + } + + applicationDetailViewNextable = applicationDetailInputValidation.asDriver(onErrorJustReturn: false) + + + editViewExitButtonClicked + .subscribe(onNext: { [weak self] in + self?.editPostCoordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + let inputValidationResult = Observable + .zip( + workTimeAndPayInputValidation.map({ + (RegisterRecruitmentPostInputSection.workTimeAndPay, $0) + }), + addressInputValidation.map({ + (RegisterRecruitmentPostInputSection.addressInfo, $0) + }), + customerRequirementInputValidation.map({ + (RegisterRecruitmentPostInputSection.customerRequirement, $0) + }), + customerInformationInputValidation.map({ + (RegisterRecruitmentPostInputSection.customerInformation, $0) + }), + applicationDetailInputValidation.map({ + (RegisterRecruitmentPostInputSection.applicationDetail, $0) + }) + ) + .map { (v1, v2, v3, v4, v5) -> RegisterRecruitmentPostInputSection? in + + for validation in [v1, v2, v3, v4, v5] { + if !validation.1 { + return validation.0 + } + } + return nil + } + .share() + + + let inputValidationSuccess = inputValidationResult.filter { $0 == nil } + let inputValidationFailure = inputValidationResult.compactMap { $0 } + + let editingRequestResult = inputValidationSuccess + .flatMap { [weak self] _ -> Single> in + guard let self else { return .never() } + + return recruitmentPostUseCase.editRecruitmentPost( + id: id, + inputs: .init( + workTimeAndPay: editing_workTimeAndPay.value, + customerRequirement: editing_customerRequirement.value, + customerInformation: editing_customerInformation.value, + applicationDetail: editing_applicationDetail.value, + addressInfo: editing_addressInfo.value + ) + ) + } + .share() + + let editingRequestSuccess = editingRequestResult.compactMap { $0.value } + let editingRequestFailure = editingRequestResult.compactMap { $0.error } + + editingRequestSuccess + .subscribe { [weak self] _ in + guard let self else { return } + + // 성공적으로 수정됨 + self.editPostCoordinator?.coordinatorDidFinish() + } + .disposed(by: disposeBag) + + Observable + .merge( + workTimeAndPayInputValidation.map({ + (RegisterRecruitmentPostInputSection.workTimeAndPay, $0) + }), + addressInputValidation.map({ + (RegisterRecruitmentPostInputSection.addressInfo, $0) + }), + customerRequirementInputValidation.map({ + (RegisterRecruitmentPostInputSection.customerRequirement, $0) + }), + customerInformationInputValidation.map({ + (RegisterRecruitmentPostInputSection.customerInformation, $0) + }), + applicationDetailInputValidation.map({ + (RegisterRecruitmentPostInputSection.applicationDetail, $0) + }) + ) + .subscribe { [weak self] inputSection, isValid in + self?.validationStateQueue.sync { [weak self] in + self?.validationState[inputSection] = isValid + } + } + .disposed(by: disposeBag) + + // 최초 데이터를 가져옵니다. + let recruitmentDetailRequestResult = recruitmentPostUseCase + .getPostDetailForCenter(id: id) + + let recruitmentDetailRequestSuccess = recruitmentDetailRequestResult.compactMap { $0.value } + let recruitmentDetailRequestFailure = recruitmentDetailRequestResult.compactMap { $0.error } + + // 인풋이 유효하지 않은 경우 + self.requestSaveFailure = Observable.merge( + inputValidationFailure.map { $0.alertMessaage }, + editingRequestFailure.map { $0.message }, + recruitmentDetailRequestFailure.asObservable().map { $0.message } + ) + .map({ message in + DefaultAlertContentVO(title: "공고 수정 오류", message: message) + }) + .asDriver(onErrorJustReturn: .default) + + + recruitmentDetailRequestSuccess + .subscribe(onSuccess: { [weak self] bundle in + guard let self else { return } + + editing_addressInfo.accept(bundle.addressInfo) + editing_applicationDetail.accept(bundle.applicationDetail) + editing_customerInformation.accept(bundle.customerInformation) + editing_customerRequirement.accept(bundle.customerRequirement) + editing_workTimeAndPay.accept(bundle.workTimeAndPay) + + }) + .disposed(by: disposeBag) + } + +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift new file mode 100644 index 00000000..ac2dd707 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift @@ -0,0 +1,228 @@ +// +// PostDetailForCenterVM.swift +// CenterFeature +// +// Created by choijunios on 8/14/24. +// + +import Foundation +import RxSwift +import RxCocoa +import Entity +import PresentationCore +import UseCaseInterface +import BaseFeature + +public protocol PostDetailViewModelable: + AnyObject, + PostDetailDisplayingViewModelable +{ + + var workerEmployCardVO: Driver? { get } + var requestDetailFailure: Driver? { get } + var applicantCount: Int? { get } + + // Input + var postEditButtonClicked: PublishRelay { get } + var exitButtonClicked: PublishRelay { get } + var checkApplicationButtonClicked: PublishRelay { get } + var viewWillAppear: PublishRelay { get } +} + +public class PostDetailForCenterVM: PostDetailViewModelable { + + weak var coordinator: PostDetailForCenterCoordinator? + + // MARK: DetailVC Interaction + public var applicantCount: Int? + public var workerEmployCardVO: RxCocoa.Driver? + public var requestDetailFailure: RxCocoa.Driver? + + public var postEditButtonClicked: RxRelay.PublishRelay = .init() + public var exitButtonClicked: RxRelay.PublishRelay = .init() + public var checkApplicationButtonClicked: RxRelay.PublishRelay = .init() + public var viewWillAppear: RxRelay.PublishRelay = .init() + + // MARK: fetched + private let fetched_workTimeAndPay: BehaviorRelay = .init(value: .init()) + private let fetched_customerRequirement: BehaviorRelay = .init(value: .init()) + private let fetched_customerInformation: BehaviorRelay = .init(value: .init()) + private let fetched_applicationDetail: BehaviorRelay = .init(value: .init()) + private let fetched_addressInfo: BehaviorRelay = .init(value: .init()) + + // MARK: Casting + public var casting_workTimeAndPay: Driver + public var casting_customerRequirement: Driver + public var casting_customerInformation: Driver + public var casting_applicationDetail: Driver + public var casting_addressInput: Driver + + + // MARK: 모든 섹션의 유효성 확인 + private let validationStateQueue = DispatchQueue.global(qos: .userInteractive) + private var validationState: [RegisterRecruitmentPostInputSection: Bool] = { + var dict: [RegisterRecruitmentPostInputSection: Bool] = [:] + RegisterRecruitmentPostInputSection.allCases.forEach { section in + dict[section] = false + } + return dict + }() + + let disposeBag = DisposeBag() + + init( + id: String, + applicantCount: Int?, + coordinator: PostDetailForCenterCoordinator?, + recruitmentPostUseCase: RecruitmentPostUseCase + ) + { + + self.coordinator = coordinator + self.applicantCount = applicantCount + + casting_workTimeAndPay = fetched_workTimeAndPay.asDriver() + casting_customerRequirement = fetched_customerRequirement.asDriver() + casting_customerInformation = fetched_customerInformation.asDriver() + casting_applicationDetail = fetched_applicationDetail.asDriver() + casting_addressInput = fetched_addressInfo.asDriver() + + // MARK: Post card + + + + // MARK: Detail View + let fetchPostDetailResult = viewWillAppear + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase + .getPostDetailForCenter(id: id) + } + .share() + + let fetchPostDetailSuccess = fetchPostDetailResult.compactMap { $0.value } + let fetchPostDetailFailure = fetchPostDetailResult.compactMap { $0.error } + + requestDetailFailure = fetchPostDetailFailure + .map({ error in + DefaultAlertContentVO( + title: "공고 상세보기 오류", + message: error.message + ) + }) + .asDriver(onErrorJustReturn: .default) + + workerEmployCardVO = fetchPostDetailSuccess + .map { [weak self] bundle in + guard let self else { return .default } + + fetched_workTimeAndPay.accept(bundle.workTimeAndPay) + fetched_customerRequirement.accept(bundle.customerRequirement) + fetched_customerInformation.accept(bundle.customerInformation) + fetched_applicationDetail.accept(bundle.applicationDetail) + fetched_addressInfo.accept(bundle.addressInfo) + + // 남은 일수 + var leftDay: Int? = nil + let calendar = Calendar.current + let currentDate = Date() + + if fetched_applicationDetail.value.applyDeadlineType == .specificDate, let deadlineDate = fetched_applicationDetail.value.deadlineDate { + + let component = calendar.dateComponents([.day], from: currentDate, to: deadlineDate) + leftDay = component.day + } + + // 초보가능 여부 + let isBeginnerPossible = fetched_applicationDetail.value.experiencePreferenceType == .beginnerPossible + + // 제목(=도로명주소) + let title = fetched_addressInfo.value.addressInfo?.roadAddress.emptyDefault("위치정보 표기 오류") ?? "" + + // 도보시간 + let timeTakenForWalk = "도보 n분" + + // 생년 + let birthYear = Int(fetched_customerInformation.value.birthYear) ?? 1970 + let currentYear = calendar.component(.year, from: currentDate) + let targetAge = currentYear - birthYear + 1 + + // 요양등급 + let targetLavel: Int = (fetched_customerInformation.value.careGrade?.rawValue ?? 0)+1 + + // 성별 + let targetGender = fetched_customerInformation.value.gender + + // 근무 요일 + let days = fetched_workTimeAndPay.value.selectedDays.filter { (_, value) in + value + }.map { (key, _) in + key + } + + // 근무 시작, 종료시간 + let startTime = fetched_workTimeAndPay.value.workStartTime?.convertToStringForButton() ?? "00:00" + let workEndTime = fetched_workTimeAndPay.value.workEndTime?.convertToStringForButton() ?? "00:00" + + // 급여타입및 양 + let paymentType = fetched_workTimeAndPay.value.paymentType ?? .hourly + let paymentAmount = fetched_workTimeAndPay.value.paymentAmount + + return WorkerEmployCardVO( + dayLeft: leftDay ?? 0, + isBeginnerPossible: isBeginnerPossible, + title: title, + timeTakenForWalk: timeTakenForWalk, + targetAge: targetAge, + targetLevel: targetLavel, + targetGender: targetGender ?? .notDetermined, + days: days, + startTime: startTime, + endTime: workEndTime, + paymentType: paymentType, + paymentAmount: paymentAmount + ) + } + .asDriver(onErrorJustReturn: .default) + + + postEditButtonClicked + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.showPostEditScreen(postId: id) + }) + .disposed(by: disposeBag) + + exitButtonClicked + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + checkApplicationButtonClicked + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.showCheckApplicantScreen(postId: id) + }) + .disposed(by: disposeBag) + } + + public func allInputsValid() -> Single { + + Single.create { [weak self] single in + + self?.validationStateQueue.sync { [weak self, single] in + + guard let self else { return } + + for (key, value) in validationState { + + if !value { + single(.success(.init(title: "입력 정보 오류", message: key.alertMessaage))) + } + } + + single(.success(nil)) + } + + return Disposables.create { } + } + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift index f0425a97..03d4f992 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift @@ -36,12 +36,28 @@ public enum RegisterRecruitmentPostInputSection: CaseIterable { } public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { - + //Init let recruitmentPostUseCase: RecruitmentPostUseCase + public weak var registerRecruitmentPostCoordinator: (any PresentationCore.RegisterRecruitmentPostCoordinatable)? + + // MARK: Edit Screen + public weak var editPostCoordinator: EditPostCoordinator? + public var editViewExitButtonClicked: RxRelay.PublishRelay = .init() + public var saveButtonClicked: RxRelay.PublishRelay = .init() + public var requestSaveFailure: RxCocoa.Driver? + + // MARK: OverView Screen + public weak var postOverviewCoordinator: PostOverviewCoordinator? + public var postEditButtonClicked: PublishRelay = .init() + public var overViewExitButtonClicked: PublishRelay = .init() + public var registerButtonClicked: PublishRelay = .init() + public var overViewWillAppear: RxRelay.PublishRelay = .init() + + public let workerEmployCardVO: Driver? // MARK: register request - public var postRegistrationSuccess: Driver + public var postRegistrationSuccess: Driver? // MARK: State var state_workTimeAndPay: WorkTimeAndPayStateObject = .init() @@ -51,11 +67,11 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { var state_addressInfo: AddressInputStateObject = .init() // MARK: Editing - let editing_workTimeAndPay: BehaviorRelay = .init(value: .init()) - let editing_customerRequirement: BehaviorRelay = .init(value: .init()) - let editing_customerInformation: BehaviorRelay = .init(value: .init()) - let editing_applicationDetail: BehaviorRelay = .init(value: .init()) - let editing_addressInfo: BehaviorRelay = .init(value: .init()) + private let editing_workTimeAndPay: BehaviorRelay = .init(value: .init()) + private let editing_customerRequirement: BehaviorRelay = .init(value: .init()) + private let editing_customerInformation: BehaviorRelay = .init(value: .init()) + private let editing_applicationDetail: BehaviorRelay = .init(value: .init()) + private let editing_addressInfo: BehaviorRelay = .init(value: .init()) // MARK: Casting public var casting_addressInput: Driver @@ -110,14 +126,9 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { public var applicationDetailViewNextable: Driver - // MARK: PostCard - public let workerEmployCardVO: Driver - // MARK: Alert - public var alert: Driver + public var alert: Driver? - // MARK: 공고등록 요청 결과 - private let requestRegistrationResult = PublishRelay>() // MARK: 모든 섹션의 유효성 확인 private let validationStateQueue = DispatchQueue.global(qos: .userInteractive) @@ -132,8 +143,11 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { // 옵셔널한 입력을 유지합니다. let disposeBag = DisposeBag() - public init(recruitmentPostUseCase: RecruitmentPostUseCase) { - + public init( + registerRecruitmentPostCoordinator: RegisterRecruitmentPostCoordinatable, + recruitmentPostUseCase: RecruitmentPostUseCase + ) { + self.registerRecruitmentPostCoordinator = registerRecruitmentPostCoordinator self.recruitmentPostUseCase = recruitmentPostUseCase // MARK: Work time and pay @@ -369,7 +383,8 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { applicationDetailViewNextable = applicationDetailInputValidation.asDriver(onErrorJustReturn: false) - // MARK: PostCard + // MARK: ----- Over view ----- + workerEmployCardVO = Observable .create { [ editing_workTimeAndPay, @@ -444,16 +459,86 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { return Disposables.create { } } .asDriver(onErrorJustReturn: .mock) + + overViewWillAppear + .subscribe(onNext: { [weak self] _ in + // OverView가 나타날때 마다 상태를 업데이트 합니다. + self?.fetchFromState() + }) + .disposed(by: disposeBag) + + postEditButtonClicked + .subscribe(onNext: { [weak self] _ in + self?.registerRecruitmentPostCoordinator?.showEditPostScreen() + }) + .disposed(by: disposeBag) + + overViewExitButtonClicked + .subscribe(onNext: { [weak self] _ in + self?.postOverviewCoordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + // MARK: ----- Edit ----- + editViewExitButtonClicked + .subscribe(onNext: { [weak self] in + self?.editPostCoordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + let requestSaveResult = saveButtonClicked + .flatMap { [weak self] _ in + self?.allInputsValid() ?? .just(.default) + } + .share() + + let requestSaveSuccess = requestSaveResult.filter { $0 == nil } + requestSaveSuccess + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + updateToState() + + // 저장이 성공적으로 완료되어 코디네이터와 뷰컨트롤러 종료 + editPostCoordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) - let shareResult = requestRegistrationResult.share() + self.requestSaveFailure = requestSaveResult + .compactMap { $0 } + .asDriver(onErrorJustReturn: .default) + - postRegistrationSuccess = shareResult + // MARK: ----------------- + let registerPostResult = registerButtonClicked + .flatMap { [weak self] _ -> Single> in + guard let self else { return .never() } + + // 공고를 등록합니다. + let inputs = RegisterRecruitmentPostBundle( + workTimeAndPay: state_workTimeAndPay, + customerRequirement: state_customerRequirement, + customerInformation: state_customerInformation, + applicationDetail: state_applicationDetail, + addressInfo: state_addressInfo + ) + + return recruitmentPostUseCase + .registerRecruitmentPost(inputs: inputs) + } + .share() + + // 공고 등록 성공 + registerPostResult .compactMap { $0.value } - .asDriver(onErrorRecover: { error in fatalError() }) + .subscribe { [weak self] _ in + self?.registerRecruitmentPostCoordinator?.showRegisterCompleteScreen() + } + .disposed(by: disposeBag) + - let requestRegistrationFailure = shareResult + let requestRegistrationFailure = registerPostResult .compactMap { $0.error } alert = requestRegistrationFailure @@ -484,7 +569,7 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { }) ) .subscribe { [weak self] inputSection, isValid in - self?.validationStateQueue.async { [weak self] in + self?.validationStateQueue.sync { [weak self] in self?.validationState[inputSection] = isValid } } @@ -505,10 +590,7 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { for (key, value) in validationState { if !value { - single(.success(.init( - title: "입력 정보 오류", - message: key.alertMessaage - ))) + single(.success(.init(title: "입력 정보 오류", message: key.alertMessaage))) } } @@ -536,22 +618,4 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { state_applicationDetail = editing_applicationDetail.value.copy() as! ApplicationDetailStateObject state_addressInfo = editing_addressInfo.value.copy() as! AddressInputStateObject } - - public func requestRegisterPost() { - - // 공고를 등록합니다. - let inputs = RegisterRecruitmentPostBundle( - workTimeAndPay: state_workTimeAndPay, - customerRequirement: state_customerRequirement, - customerInformation: state_customerInformation, - applicationDetail: state_applicationDetail, - addressInfo: state_addressInfo - ) - - recruitmentPostUseCase - .registerRecruitmentPost(inputs: inputs) - .asObservable() - .bind(to: requestRegistrationResult) - .disposed(by: disposeBag) - } } diff --git a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift index 015452b5..71ab8589 100644 --- a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift @@ -6,18 +6,63 @@ // import UIKit +import RootFeature +import ConcreteUseCase +import ConcreteRepository +import NetworkDataSource class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + var coordinator: RegisterRecruitmentPostCoordinator! + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } + let store = TestStore() + + try! store.saveAuthToken( + accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMzYyMTQ5MiwibmJmIjoxNzIzNjIxNDkyLCJleHAiOjE3MjM2MjIwOTIsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkxNGZjMi04YTk4LTdhNDAtYWFmYS04OWM0MDhiZmEyOGMiLCJwaG9uZU51bWJlciI6IjAxMC00NDQ0LTUyMzIiLCJ1c2VyVHlwZSI6ImNlbnRlciJ9.WVD8-17nNTewK1EAARw_s-rxfs-6n1pZTyqCdvseIW8", + refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMzYyMDAzNywibmJmIjoxNzIzNjIwMDM3LCJleHAiOjE3MjQ4Mjk2MzcsInR5cGUiOiJSRUZSRVNIX1RPS0VOIiwidXNlcklkIjoiMDE5MTRmYzItOGE5OC03YTQwLWFhZmEtODljNDA4YmZhMjhjIiwidXNlclR5cGUiOiJjZW50ZXIifQ.hlnjMjEGDD11_XAR2QlfiT1awQoccvE04aqhkZUmWTc" + ) + + let nav = UINavigationController() + nav.setNavigationBarHidden(true, animated: false) + + self.coordinator = RegisterRecruitmentPostCoordinator( + dependency: .init( + navigationController: nav, + recruitmentPostUseCase: DefaultRecruitmentPostUseCase( + repository: DefaultRecruitmentPostRepository(store) + ) + ) + ) window = UIWindow(windowScene: windowScene) - window?.rootViewController = ViewController() + window?.rootViewController = nav window?.makeKeyAndVisible() + + coordinator.start() } } + +class TestStore: KeyValueStore { + func save(key: String, value: String) throws { + UserDefaults.standard.setValue(value, forKey: key) + } + + func get(key: String) -> String? { + UserDefaults.standard.string(forKey: key) + } + + func delete(key: String) throws { + + } + + func removeAll() throws { + + } + +} 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 57671e8a..90bde115 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 @@ -18,11 +18,18 @@ public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatabl weak var parent: CenterMainCoordinatable? let navigationController: UINavigationController let workerProfileUseCase: WorkerProfileUseCase + let recruitmentPostUseCase: RecruitmentPostUseCase - public init(parent: CenterMainCoordinatable? = nil, navigationController: UINavigationController, workerProfileUseCase: WorkerProfileUseCase) { + public init( + parent: CenterMainCoordinatable? = nil, + navigationController: UINavigationController, + workerProfileUseCase: WorkerProfileUseCase, + recruitmentPostUseCase: RecruitmentPostUseCase + ) { self.parent = parent self.navigationController = navigationController self.workerProfileUseCase = workerProfileUseCase + self.recruitmentPostUseCase = recruitmentPostUseCase } } @@ -35,6 +42,7 @@ public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatabl public weak var parent: CenterMainCoordinatable? let workerProfileUseCase: WorkerProfileUseCase + let recruitmentPostUseCase: RecruitmentPostUseCase public init( dependency: Dependency @@ -42,6 +50,7 @@ public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatabl self.parent = dependency.parent self.navigationController = dependency.navigationController self.workerProfileUseCase = dependency.workerProfileUseCase + self.recruitmentPostUseCase = dependency.recruitmentPostUseCase } public func start() { @@ -60,11 +69,11 @@ public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatabl public extension RecruitmentManagementCoordinator { - func showCheckingApplicantScreen(_ centerEmployCardVO: CenterEmployCardVO) { + func showCheckingApplicantScreen(postId: String) { let coordinator = CheckApplicantCoordinator( dependency: .init( navigationController: navigationController, - centerEmployCardVO: centerEmployCardVO, + centerEmployCardVO: .mock, workerProfileUseCase: workerProfileUseCase ) ) @@ -72,4 +81,37 @@ public extension RecruitmentManagementCoordinator { coordinator.parent = self coordinator.start() } + + func showPostDetailScreenForCenter(postId: String, applicantCount: Int?) { + + let coordinator = PostDetailForCenterCoordinator( + dependency: .init( + postId: postId, + applicantCount: applicantCount, + navigationController: navigationController, + recruitmentPostUseCase: recruitmentPostUseCase + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } + + func showEditScreen(postId: String) { + + let vm = EditPostVM( + id: postId, + recruitmentPostUseCase: recruitmentPostUseCase + ) + let coordinator = EditPostCoordinator( + dependency: .init( + navigationController: navigationController, + viewModel: vm + ) + ) + vm.editPostCoordinator = coordinator + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } } diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RegisterRecruitmentPostCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RegisterRecruitmentPostCoordinator.swift new file mode 100644 index 00000000..ca84ed61 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RegisterRecruitmentPostCoordinator.swift @@ -0,0 +1,95 @@ +// +// asd.swift +// RootFeature +// +// Created by choijunios on 8/14/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity +import CenterFeature +import WorkerFeature +import BaseFeature + + +public class RegisterRecruitmentPostCoordinator: RegisterRecruitmentPostCoordinatable { + + public struct Dependency { + let navigationController: UINavigationController + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.navigationController = navigationController + self.recruitmentPostUseCase = recruitmentPostUseCase + } + } + + public var childCoordinators: [Coordinator] = [] + + public var parent: ParentCoordinator? + + public var navigationController: UINavigationController + + var viewControllerRef: UIViewController? + var registerRecruitmentPostVM: RegisterRecruitmentPostViewModelable! + + public init(dependency: Dependency) { + self.navigationController = dependency.navigationController + self.registerRecruitmentPostVM = RegisterRecruitmentPostVM( + registerRecruitmentPostCoordinator: self, + recruitmentPostUseCase: dependency.recruitmentPostUseCase + ) + } + + public func start() { + let vc = RegisterRecruitmentPostVC() + + vc.bind(viewModel: registerRecruitmentPostVM) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } +} + +public extension RegisterRecruitmentPostCoordinator { + + func showEditPostScreen() { + let coordinator = EditPostCoordinator( + dependency: .init( + navigationController: navigationController, + viewModel: registerRecruitmentPostVM + ) + ) + coordinator.parent = self + addChildCoordinator(coordinator) + coordinator.start() + } + + func showOverViewScreen() { + let coordinator = PostOverviewCoordinator( + dependency: .init( + navigationController: navigationController, + viewModel: registerRecruitmentPostVM + ) + ) + coordinator.parent = self + addChildCoordinator(coordinator) + coordinator.start() + } + + func showRegisterCompleteScreen() { + let coordinator = RegisterCompleteCoordinator( + navigationController: navigationController + ) + coordinator.parent = self + addChildCoordinator(coordinator) + coordinator.start() + } + + func registerFinished() { + clearChildren() + popViewController(animated: false) + parent?.removeChildCoordinator(self) + } +} diff --git a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift index f4d784fc..721d2aa6 100644 --- a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift @@ -24,7 +24,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let store = TestStore() try! store.saveAuthToken( - accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMzI5NzQyOCwibmJmIjoxNzIzMjk3NDI4LCJleHAiOjE3MjMyOTgwMjgsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkxM2M4Ny1mMjMxLTdmMDctYTNjYS1jYTJhNjk4OTExZWQiLCJwaG9uZU51bWJlciI6IjAxMC0zMzI2LTU2NzgiLCJ1c2VyVHlwZSI6ImNhcmVyIn0.R_obT8lFGxf2jeDhrpC0aLntwDslI-NYoEbyEZLwhZ0", + accessToken: "", refreshToken: "" ) diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift index 5ca23ec9..22a192c2 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift @@ -9,5 +9,7 @@ import Entity public protocol RecruitmentManagementCoordinatable: ParentCoordinator { - func showCheckingApplicantScreen(_ centerEmployCardVO: CenterEmployCardVO) + func showCheckingApplicantScreen(postId: String) + func showPostDetailScreenForCenter(postId: String, applicantCount: Int?) + func showEditScreen(postId: String) } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RegisterRecruitmentPostCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RegisterRecruitmentPostCoordinatable.swift index 0c82422c..9cf386f9 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RegisterRecruitmentPostCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RegisterRecruitmentPostCoordinatable.swift @@ -10,6 +10,7 @@ import Foundation public protocol RegisterRecruitmentPostCoordinatable: ParentCoordinator { func showOverViewScreen() + func showEditPostScreen() func showRegisterCompleteScreen() func registerFinished() }