diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift index eb2538f4..d10f28d4 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift @@ -64,9 +64,11 @@ extension CenterProfileRegisterCoordinator { func showMyCenterProfile() { let coordinator = CenterProfileCoordinator( - mode: .myProfile, - profileUseCase: injector.resolve(CenterProfileUseCase.self), - navigationController: navigationController + dependency: .init( + mode: .myProfile, + profileUseCase: injector.resolve(CenterProfileUseCase.self), + navigationController: navigationController + ) ) addChildCoordinator(coordinator) coordinator.parent = self diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift index 8f1578fb..2716a55d 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift @@ -9,6 +9,7 @@ import UIKit import DSKit import PresentationCore import RootFeature +import UseCaseInterface class WorkerMainCoordinator: ParentCoordinator { @@ -54,7 +55,7 @@ class WorkerMainCoordinator: ParentCoordinator { func createNavForTab(page: IdleWorkerMainPage) -> UINavigationController { let tabNavController = UINavigationController() - tabNavController.setNavigationBarHidden(false, animated: false) + tabNavController.setNavigationBarHidden(true, animated: false) startTabCoordinator( page: page, @@ -66,19 +67,27 @@ class WorkerMainCoordinator: ParentCoordinator { // #2. 생성한 컨트롤러를 각 탭별 Coordinator에 전달 func startTabCoordinator(page: IdleWorkerMainPage, navigationController: UINavigationController) { - var coordinator: ChildCoordinator! + var coordinator: Coordinator! switch page { case .home: - coordinator = RecruitmentBoardCoordinator( - navigationController: navigationController + coordinator = WorkerRecruitmentBoardCoordinator( + depedency: .init( + navigationController: navigationController, + centerProfileUseCase: injector.resolve(CenterProfileUseCase.self), + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + ) ) case .preferredPost: - coordinator = ApplyManagementCoordinator( - navigationController: navigationController + coordinator = AppliedAndLikedBoardCoordinator( + depedency: .init( + navigationController: navigationController, + centerProfileUseCase: injector.resolve(CenterProfileUseCase.self), + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + ) ) case .setting: - coordinator = RecruitmentBoardCoordinator( + coordinator = SettingCoordinator( navigationController: navigationController ) } diff --git a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift index 090e05e4..c616a412 100644 --- a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift +++ b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift @@ -10,6 +10,7 @@ import RxSwift import Entity import NetworkDataSource import Foundation +import Moya public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { @@ -21,6 +22,7 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { } } + // MARK: Center public func registerPost(bundle: RegisterRecruitmentPostBundle) -> RxSwift.Single { let encodedData = try! JSONEncoder().encode(bundle.toDTO()) @@ -47,6 +49,36 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { with: .withToken ).map { _ in () } } + + // MARK: Worker + public func getPostDetailForWorker(id: String) -> RxSwift.Single { + service.request( + api: .postDetail(id: id, userType: .worker), + with: .withToken + ) + .map(RecruitmentPostDTO.self) + .map { dto in + dto.toEntity() + } + } + + public func getNativePostListForWorker(nextPageId: String?, requestCnt: Int = 10) -> RxSwift.Single { + + service.request( + api: .nativePostList(nextPageId: nextPageId, requestCnt: String(requestCnt)), + with: .withToken + ) + .map(RecruitmentPostListForWorkerDTO.self) + .catch({ error in + if let moyaError = error as? MoyaError, case .objectMapping(let error, let response) = moyaError { + print(error.localizedDescription) + } + return .error(error) + }) + .map { dto in + dto.toEntity() + } + } } fileprivate extension RegisterRecruitmentPostBundle { diff --git a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift index 6051ba67..cf891f1b 100644 --- a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift @@ -29,7 +29,7 @@ public enum AuthAPI { extension AuthAPI: BaseAPI { - public var apiType: APIType { .auth} + public var apiType: APIType { .auth } public var method: Moya.Method { diff --git a/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift b/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift index 13b7d1e4..3a1169d3 100644 --- a/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/RcruitmentPostAPI.swift @@ -7,6 +7,7 @@ import Moya import Foundation +import Alamofire import Entity public enum RcruitmentPostAPI { @@ -19,6 +20,9 @@ public enum RcruitmentPostAPI { case editPost(id: String, postData: Data) case removePost(id: String) case closePost(id: String) + + // Worker + case nativePostList(nextPageId: String?, requestCnt: String) } extension RcruitmentPostAPI: BaseAPI { @@ -39,6 +43,8 @@ extension RcruitmentPostAPI: BaseAPI { "/\(id)" case .closePost(let id): "/\(id)/end" + case .nativePostList: + "" } } @@ -54,11 +60,29 @@ extension RcruitmentPostAPI: BaseAPI { .delete case .closePost: .patch + case .nativePostList: + .get + } + } + + var bodyParameters: Parameters? { + var params: Parameters = [:] + switch self { + case .nativePostList(let nextPageId, let requestCnt): + if let nextPageId { + params["next"] = nextPageId + } + params["limit"] = requestCnt + default: + break } + return params } var parameterEncoding: ParameterEncoding { switch self { + case .nativePostList: + return URLEncoding.queryString default: return JSONEncoding.default } @@ -66,6 +90,8 @@ extension RcruitmentPostAPI: BaseAPI { public var task: Moya.Task { switch self { + case .nativePostList: + .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) case .registerPost(let bodyData): .requestData(bodyData) case .editPost(_, let editData): diff --git a/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift index 22fa8838..8a914db2 100644 --- a/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/UserInformationAPI.swift @@ -90,9 +90,9 @@ extension UserInformationAPI: BaseAPI { .post case .getMyWorkerProfile: .get - case .getOtherWorkerProfile(id: let id): + case .getOtherWorkerProfile: .get - case .updateWorkerProfile(data: let data): + case .updateWorkerProfile: .patch } } diff --git a/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift index fd106d6c..7c17a650 100644 --- a/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift +++ b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift @@ -89,7 +89,7 @@ public struct RecruitmentPostFetchDTO: Codable { age: Int, weight: Int?, careLevel: Int, - mentalStatus: String, + mentalStatus: String, ㅇ disease: String?, lifeAssistance: [String]?, extraRequirement: String?, @@ -132,7 +132,17 @@ public struct RecruitmentPostFetchDTO: Codable { workTimeAndPay.workStartTime = IdleDateComponent.toEntity(text: startTime) workTimeAndPay.workEndTime = IdleDateComponent.toEntity(text: endTime) workTimeAndPay.paymentType = PaymentType.toEntity(text: payType) - workTimeAndPay.paymentAmount = String(payAmount) + + let payAmount = String(payAmount) + var formedPayAmount = "" + for (index, char) in payAmount.reversed().enumerated() { + if (index % 3) == 0, index != 0 { + formedPayAmount += "," + } + formedPayAmount += String(char) + } + + workTimeAndPay.paymentAmount = formedPayAmount let addressInfo: AddressInputStateObject = .init() addressInfo.addressInfo = .init( @@ -184,7 +194,7 @@ public struct RecruitmentPostFetchDTO: Codable { } } -fileprivate extension ApplyType { +extension ApplyType { static func toEntity(text: String) -> ApplyType { switch text { case "CALLING": @@ -200,7 +210,7 @@ fileprivate extension ApplyType { } } -fileprivate extension ApplyDeadlineType { +extension ApplyDeadlineType { static func toEntity(text: String) -> ApplyDeadlineType { switch text { case "UNLIMITED": @@ -214,7 +224,7 @@ fileprivate extension ApplyDeadlineType { } } -fileprivate extension DailySupportType { +extension DailySupportType { static func toEntity(text: String) -> DailySupportType { switch text { case "CLEANING": @@ -234,7 +244,7 @@ fileprivate extension DailySupportType { } } -fileprivate extension Gender { +extension Gender { static func toEntity(text: String) -> Gender { switch text { case "MAN": @@ -248,7 +258,7 @@ fileprivate extension Gender { } } -fileprivate extension PaymentType { +extension PaymentType { static func toEntity(text: String) -> PaymentType { switch text { @@ -265,7 +275,7 @@ fileprivate extension PaymentType { } } -fileprivate extension IdleDateComponent { +extension IdleDateComponent { static func toEntity(text: String) -> IdleDateComponent { let timeArr = text.split(separator: ":") @@ -283,7 +293,7 @@ fileprivate extension IdleDateComponent { } } -fileprivate extension CognitionDegree { +extension CognitionDegree { static func toEntity(text: String) -> CognitionDegree { switch text { case "NORMAL": @@ -299,7 +309,7 @@ fileprivate extension CognitionDegree { } } -fileprivate extension WorkDay { +extension WorkDay { static func toEntity(text: String) -> WorkDay { diff --git a/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift new file mode 100644 index 00000000..164df625 --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift @@ -0,0 +1,131 @@ +// +// RecruitmentPostDetailForWorkerDTO.swift +// NetworkDataSource +// +// Created by choijunios on 8/15/24. +// + +import Foundation +import Entity + +public struct RecruitmentPostDTO: Codable { + public let id: String + + public let longitude: String + public let latitude: String + + public let centerId: String + public let centerName: String + public let centerRoadNameAddress: String + + public let distance: Int + + public let isMealAssistance: Bool + public let isBowelAssistance: Bool + public let isWalkingAssistance: Bool + public let isExperiencePreferred: Bool + + public let weekdays: [String] + public let startTime: String + public let endTime: String + public let payType: String + public let payAmount: Int + public let roadNameAddress: String + public let lotNumberAddress: String + public let 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 applyDeadlineType: String + public let applyDeadline: String? + + public func toEntity() -> RecruitmentPostForWorkerBundle { + + 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) + + let payAmount = String(payAmount) + var formedPayAmount = "" + for (index, char) in payAmount.reversed().enumerated() { + if (index % 3) == 0, index != 0 { + formedPayAmount += "," + } + formedPayAmount += String(char) + } + workTimeAndPay.paymentAmount = formedPayAmount + + let addressInfo: AddressInputStateObject = .init() + addressInfo.addressInfo = .init( + roadAddress: roadNameAddress, + jibunAddress: lotNumberAddress + ) + + let customerInfo: CustomerInformationStateObject = .init() + 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 + + // MARK: CenterInfo + let centerInfo: RecruitmentPostForWorkerBundle.CenterInfo = .init( + centerId: centerId, + centerName: centerName, + centerRoadAddress: centerRoadNameAddress + ) + + let jobLocation: LocationInformation = .init( + longitude: Double(longitude)!, + latitude: Double(latitude)! + ) + + return .init( + workTimeAndPay: workTimeAndPay, + customerRequirement: customerRequirement, + customerInformation: customerInfo, + applicationDetail: applicationDetail, + addressInfo: addressInfo, + centerInfo: centerInfo, + jobLocation: jobLocation, + distanceToWorkPlace: distance + ) + } +} + diff --git a/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift new file mode 100644 index 00000000..47ca3703 --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift @@ -0,0 +1,88 @@ +// +// RecuritmentPostListForWorkerDTO.swift +// NetworkDataSource +// +// Created by choijunios on 8/16/24. +// + +import Foundation +import Entity + +public struct RecruitmentPostListForWorkerDTO: Codable { + + public let items: [RecruitmentPostForWorkerDTO] + public let next: String? + public let total: Int + + public func toEntity() -> RecruitmentPostListForWorkerVO { + + return .init( + posts: items.map { $0.toEntity() }, + nextPageId: next, + fetchedPostCount: total + ) + } +} + +public struct RecruitmentPostForWorkerDTO: Codable { + public let isExperiencePreferred: Bool + public let id: String + public let weekdays: [String] + public let startTime: String + public let endTime: String + public let payType: String + public let payAmount: Int + public let roadNameAddress: String + public let lotNumberAddress: String + public let gender: String + public let age: Int + public let careLevel: Int + public let applyDeadlineType: String + public let applyDeadline: String? + public let distance: Int + + public func toEntity() -> RecruitmentPostForWorkerVO { + + let workDayList = weekdays.map({ dayText in + WorkDay.toEntity(text: dayText) + }) + + let payAmount = String(payAmount) + var formedPayAmount = "" + for (index, char) in payAmount.reversed().enumerated() { + if (index % 3) == 0, index != 0 { + formedPayAmount += "," + } + formedPayAmount += String(char) + } + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let deadlineDate = applyDeadline != nil ? dateFormatter.date(from: applyDeadline!) : nil + + // distance는 미터단위입니다. + var distanceText: String = "\(distance)m" + if distance >= 1000 { + let kilometers = Double(distance)/1000.0 + distanceText = String(format: "%.1fkm", kilometers) + } + + return .init( + postId: id, + workDays: workDayList, + startTime: startTime, + endTime: endTime, + roadNameAddress: roadNameAddress, + lotNumberAddress: lotNumberAddress, + gender: Gender.toEntity(text: gender), + age: age, + cardGrade: CareGrade(rawValue: careLevel-1)!, + isExperiencePreferred: isExperiencePreferred, + applyDeadlineType: ApplyDeadlineType.toEntity(text: applyDeadlineType), + applyDeadlineDate: deadlineDate, + payType: PaymentType.toEntity(text: payType), + payAmount: formedPayAmount, + distanceFromWorkPlace: distanceText + ) + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift index 5c95aa67..d78b8046 100644 --- a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift @@ -49,8 +49,29 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { ) } - public func getPostDetailForCenter(id: String) -> RxSwift.Single> { convert(task: repository.getPostDetailForCenter(id: id)) } + + public func getPostDetailForWorker(id: String) -> RxSwift.Single> { + convert(task: repository.getPostDetailForWorker(id: id)) + } + + public func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> { + + let stream: Single! + + switch request { + case .native(let nextPageId): + stream = repository.getNativePostListForWorker( + nextPageId: nextPageId, + requestCnt: postCount + ) + case .thirdParty(let nextPageId): + /// 미구현 + fatalError() + } + + return convert(task: stream) + } } diff --git a/project/Projects/Domain/Entity/State/RecruitmentPost/Paging/PostPagingRequestForWorker.swift b/project/Projects/Domain/Entity/State/RecruitmentPost/Paging/PostPagingRequestForWorker.swift new file mode 100644 index 00000000..e2c2602d --- /dev/null +++ b/project/Projects/Domain/Entity/State/RecruitmentPost/Paging/PostPagingRequestForWorker.swift @@ -0,0 +1,13 @@ +// +// PostPagingRequestForWorker.swift +// Entity +// +// Created by choijunios on 8/17/24. +// + +import Foundation + +public enum PostPagingRequestForWorker { + case native(nextPageId: String?) + case thirdParty(nextPageId: String?) +} diff --git a/project/Projects/Domain/Entity/VO/AlertContentVO.swift b/project/Projects/Domain/Entity/VO/AlertContentVO.swift index 7a4d34fe..d174b0d5 100644 --- a/project/Projects/Domain/Entity/VO/AlertContentVO.swift +++ b/project/Projects/Domain/Entity/VO/AlertContentVO.swift @@ -22,3 +22,32 @@ public struct DefaultAlertContentVO { message: "동작을 수행하지 못했습니다." ) } + +public struct AlertWithCompletionVO { + + public typealias AlertCompletion = () -> () + + public let title: String + public let message: String + public let buttonInfo: [(String, AlertCompletion?)] + + public init( + title: String, + message: String, + buttonInfo: [( + String, + AlertCompletion? + )] = [ + ("닫기", nil) + ] + ) { + self.title = title + self.message = message + self.buttonInfo = buttonInfo + } + + public static let `default` = AlertWithCompletionVO( + title: "오류", + message: "동작을 수행하지 못했습니다." + ) +} diff --git a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift b/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift index 9c5d3f45..08ce2464 100644 --- a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift +++ b/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift @@ -11,10 +11,10 @@ public struct WorkerEmployCardVO { public let dayLeft: Int public let isBeginnerPossible: Bool + public let distanceFromWorkPlace: String public let title: String - public let timeTakenForWalk: String public let targetAge: Int - public let targetLevel: Int + public let careGrade: CareGrade public let targetGender: Gender public let days: [WorkDay] public let startTime: String @@ -22,13 +22,27 @@ public struct WorkerEmployCardVO { public let paymentType: PaymentType public let paymentAmount: String - public init(dayLeft: Int, isBeginnerPossible: Bool, title: String, timeTakenForWalk: String, targetAge: Int, targetLevel: Int, targetGender: Gender, days: [WorkDay], startTime: String, endTime: String, paymentType: PaymentType, paymentAmount: String) { + public init( + dayLeft: Int, + isBeginnerPossible: Bool, + distanceFromWorkPlace: String, + title: String, + targetAge: Int, + careGrade: CareGrade, + targetGender: Gender, + days: [WorkDay], + startTime: String, + endTime: String, + paymentType: PaymentType, + paymentAmount: String + ) { + self.dayLeft = dayLeft self.isBeginnerPossible = isBeginnerPossible + self.distanceFromWorkPlace = distanceFromWorkPlace self.title = title - self.timeTakenForWalk = timeTakenForWalk self.targetAge = targetAge - self.targetLevel = targetLevel + self.careGrade = careGrade self.targetGender = targetGender self.days = days self.startTime = startTime @@ -36,6 +50,109 @@ public struct WorkerEmployCardVO { self.paymentType = paymentType self.paymentAmount = paymentAmount } + + /// 서버가 입력중인 공고의 확인화면에 사용됩니다. + public static func create( + workTimeAndPay: WorkTimeAndPayStateObject, + customerRequirement: CustomerRequirementStateObject, + customerInformation: CustomerInformationStateObject, + applicationDetail: ApplicationDetailStateObject, + addressInfo: AddressInputStateObject + ) -> WorkerEmployCardVO { + + // 남은 일수 + var leftDay: Int? = nil + let calendar = Calendar.current + let currentDate = Date() + + if applicationDetail.applyDeadlineType == .specificDate, let deadlineDate = applicationDetail.deadlineDate { + + let component = calendar.dateComponents([.day], from: currentDate, to: deadlineDate) + leftDay = component.day + } + + // 초보가능 여부 + let isBeginnerPossible = applicationDetail.experiencePreferenceType == .beginnerPossible + + // 제목(=도로명주소) + let title = addressInfo.addressInfo?.roadAddress.emptyDefault("위치정보 표기 오류") ?? "" + + // 생년 + let birthYear = Int(customerInformation.birthYear) ?? 1950 + let currentYear = calendar.component(.year, from: currentDate) + let targetAge = currentYear - birthYear + 1 + + // 요양등급 + let careGrade: CareGrade = customerInformation.careGrade ?? .one + + // 성별 + let targetGender = customerInformation.gender + + // 근무 요일 + let days = workTimeAndPay.selectedDays.filter { (_, value) in + value + }.map { (key, _) in key } + + // 근무 시작, 종료시간 + let startTime = workTimeAndPay.workStartTime?.convertToStringForButton() ?? "00:00" + let workEndTime = workTimeAndPay.workEndTime?.convertToStringForButton() ?? "00:00" + + // 급여타입및 양 + let paymentType = workTimeAndPay.paymentType ?? .hourly + let paymentAmount = workTimeAndPay.paymentAmount + + return WorkerEmployCardVO( + dayLeft: leftDay ?? 31, + isBeginnerPossible: isBeginnerPossible, + distanceFromWorkPlace: "500m", + title: title, + targetAge: targetAge, + careGrade: careGrade, + targetGender: targetGender ?? .notDetermined, + days: days, + startTime: startTime, + endTime: workEndTime, + paymentType: paymentType, + paymentAmount: paymentAmount + ) + } + + public static func create(vo: RecruitmentPostForWorkerVO) -> WorkerEmployCardVO { + + // 남은 일수 + var leftDay: Int? = nil + let calendar = Calendar.current + let currentDate = Date() + + if vo.applyDeadlineType == .specificDate, let deadlineDate = vo.applyDeadlineDate { + + let component = calendar.dateComponents([.day], from: currentDate, to: deadlineDate) + leftDay = component.day + } + + return .init( + dayLeft: leftDay ?? 31, + isBeginnerPossible: !vo.isExperiencePreferred, + distanceFromWorkPlace: vo.distanceFromWorkPlace, + title: vo.roadNameAddress, + targetAge: vo.age, + careGrade: vo.cardGrade, + targetGender: vo.gender, + days: vo.workDays, + startTime: vo.startTime, + endTime: vo.endTime, + paymentType: vo.payType, + paymentAmount: vo.payAmount + ) + } +} + + +fileprivate extension String { + + func emptyDefault(_ str: String) -> String { + self.isEmpty ? str : self + } } public extension WorkerEmployCardVO { @@ -43,25 +160,25 @@ public extension WorkerEmployCardVO { static let mock = WorkerEmployCardVO( dayLeft: 10, isBeginnerPossible: true, + distanceFromWorkPlace: "500m", title: "서울특별시 강남구 신사동", - timeTakenForWalk: "도보 15분~20분", targetAge: 78, - targetLevel: 1, + careGrade: .four, targetGender: .female, days: WorkDay.allCases, startTime: "09:00", endTime: "15:00", - paymentType: .monthly, + paymentType: .hourly, paymentAmount: "12,500" ) static let `default` = WorkerEmployCardVO( dayLeft: 0, isBeginnerPossible: true, + distanceFromWorkPlace: "8km", title: "기본값", - timeTakenForWalk: "기본값", targetAge: 10, - targetLevel: 1, + careGrade: .one, targetGender: .notDetermined, days: [], startTime: "00:00", diff --git a/project/Projects/Domain/Entity/VO/Post/RecruitmentPostForWorkerBundle.swift b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostForWorkerBundle.swift new file mode 100644 index 00000000..eb60c940 --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostForWorkerBundle.swift @@ -0,0 +1,68 @@ +// +// RecruitmentPostForWorkerBundle.swift +// Entity +// +// Created by choijunios on 8/15/24. +// + +import Foundation + +/// 위도경도 좌표를 담고 있습니다. +public struct LocationInformation { + public let longitude: Double + public let latitude: Double + + public init(longitude: Double, latitude: Double) { + self.longitude = longitude + self.latitude = latitude + } +} + +public class RecruitmentPostForWorkerBundle { + + public struct CenterInfo { + public let centerId: String + public let centerName: String + public let centerRoadAddress: String + + public init(centerId: String, centerName: String, centerRoadAddress: String) { + self.centerId = centerId + self.centerName = centerName + self.centerRoadAddress = centerRoadAddress + } + } + + public let workTimeAndPay: WorkTimeAndPayStateObject + public let customerRequirement: CustomerRequirementStateObject + public let customerInformation: CustomerInformationStateObject + public let applicationDetail: ApplicationDetailStateObject + public let addressInfo: AddressInputStateObject + + /// 센터정보 + public let centerInfo: CenterInfo + + /// 근무지 위치정보 + public let distanceToWorkPlace: Int + public let jobLocation: LocationInformation + + + public init( + workTimeAndPay: WorkTimeAndPayStateObject, + customerRequirement: CustomerRequirementStateObject, + customerInformation: CustomerInformationStateObject, + applicationDetail: ApplicationDetailStateObject, + addressInfo: AddressInputStateObject, + centerInfo: CenterInfo, + jobLocation: LocationInformation, + distanceToWorkPlace: Int + ) { + self.workTimeAndPay = workTimeAndPay + self.customerRequirement = customerRequirement + self.customerInformation = customerInformation + self.applicationDetail = applicationDetail + self.addressInfo = addressInfo + self.centerInfo = centerInfo + self.jobLocation = jobLocation + self.distanceToWorkPlace = distanceToWorkPlace + } +} diff --git a/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift new file mode 100644 index 00000000..6ffcb8c1 --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift @@ -0,0 +1,96 @@ +// +// RecruitmentPostListForWorkerVO.swift +// Entity +// +// Created by choijunios on 8/16/24. +// + +import Foundation + +public struct RecruitmentPostListForWorkerVO { + + public let posts: [RecruitmentPostForWorkerVO] + public let nextPageId: String? + public let fetchedPostCount: Int + + public init(posts: [RecruitmentPostForWorkerVO], nextPageId: String?, fetchedPostCount: Int) { + self.posts = posts + self.nextPageId = nextPageId + self.fetchedPostCount = fetchedPostCount + } +} + +public struct RecruitmentPostForWorkerVO { + public let postId: String + + public let workDays: [WorkDay] + public let startTime: String + public let endTime: String + + public let roadNameAddress: String + public let lotNumberAddress: String + + public let gender: Gender + public let age: Int + public let cardGrade: CareGrade + + public let isExperiencePreferred: Bool + public let applyDeadlineType: ApplyDeadlineType + public let applyDeadlineDate: Date? + public let payType: PaymentType + public let payAmount: String + + public let distanceFromWorkPlace: String + + public init( + postId: String, + workDays: [WorkDay], + startTime: String, + endTime: String, + roadNameAddress: String, + lotNumberAddress: String, + gender: Gender, + age: Int, + cardGrade: CareGrade, + isExperiencePreferred: Bool, + applyDeadlineType: ApplyDeadlineType, + applyDeadlineDate: Date?, + payType: PaymentType, + payAmount: String, + distanceFromWorkPlace: String + ) { + self.postId = postId + self.workDays = workDays + self.startTime = startTime + self.endTime = endTime + self.roadNameAddress = roadNameAddress + self.lotNumberAddress = lotNumberAddress + self.gender = gender + self.age = age + self.cardGrade = cardGrade + self.isExperiencePreferred = isExperiencePreferred + self.applyDeadlineType = applyDeadlineType + self.applyDeadlineDate = applyDeadlineDate + self.payType = payType + self.payAmount = payAmount + self.distanceFromWorkPlace = distanceFromWorkPlace + } + + public static let mock = RecruitmentPostForWorkerVO( + postId: "test-post-id", + workDays: [.mon, .wed, .fri], + startTime: "09:00", + endTime: "17:00", + roadNameAddress: "서울시 영등포구 여등포동", + lotNumberAddress: "서울시 영등포구 여등포동", + gender: .female, + age: 54, + cardGrade: .three, + isExperiencePreferred: true, + applyDeadlineType: .specificDate, + applyDeadlineDate: Calendar.current.date(byAdding: .day, value: 7, to: Date()), + payType: .hourly, + payAmount: "15,000", + distanceFromWorkPlace: "2.5km" + ) +} diff --git a/project/Projects/Domain/Entity/State/RecruitmentPost/RegisterRecruitmentPostBundle.swift b/project/Projects/Domain/Entity/VO/Post/RegisterRecruitmentPostBundle.swift similarity index 100% rename from project/Projects/Domain/Entity/State/RecruitmentPost/RegisterRecruitmentPostBundle.swift rename to project/Projects/Domain/Entity/VO/Post/RegisterRecruitmentPostBundle.swift diff --git a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift index 0e6f0ab8..1e5cf1cf 100644 --- a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift @@ -12,9 +12,19 @@ import Entity public protocol RecruitmentPostRepository: RepositoryBase { // MARK: Center + /// 공고를 등록합니다. func registerPost(bundle: RegisterRecruitmentPostBundle) -> Single + /// 센터측에서 등록한 공고의 상세내역을 확인합니다. func getPostDetailForCenter(id: String) -> Single + /// 센터가 등록한 공고의 상세정보를 수정합니다. func editPostDetail(id: String, bundle: RegisterRecruitmentPostBundle) -> Single + + // MARK: Worker + /// 요양보호사 공고의 상세정보를 조회합니다. + func getPostDetailForWorker(id: String) -> Single + + /// 요샹보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. + func getNativePostListForWorker(nextPageId: String?, requestCnt: Int) -> Single } diff --git a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift index 737b1a79..e782bb3c 100644 --- a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift @@ -10,6 +10,8 @@ import Entity import RxSwift public protocol RecruitmentPostUseCase: UseCaseBase { + + // MARK: Center /// 센터측이 공고를 등록하는 액션입니다. func registerRecruitmentPost(inputs: RegisterRecruitmentPostBundle) -> Single> @@ -19,4 +21,17 @@ public protocol RecruitmentPostUseCase: UseCaseBase { /// 센터측이 공고를 조회하는 액션입니다. func getPostDetailForCenter(id: String) -> Single> + + + // MARK: Worker + + /// 요양보호사가 공고상세를 확인하는 경우에 호출합니다. + /// - 반환값 + /// - 공고상세정보(센터와 달리 고객 이름 배제) + /// - 근무지 위치(위경도) + /// - 센터정보(센터 id, 이름, 도로명 주소) + func getPostDetailForWorker(id: String) -> Single> + + /// 요양보호사가 메인화면에 사용할 공고리스트를 호출합니다. + func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> } diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift index 736e6c9b..9cc654d7 100644 --- a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift @@ -54,7 +54,6 @@ extension ViewController3: UITableViewDelegate, UITableViewDataSource { guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: WorkerEmployCardCell.self), for: indexPath) as? WorkerEmployCardCell else { fatalError("Unable to dequeue WorkerEmployCard") } - cell.bind(vo: .mock) return cell } diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location.imageset/Contents.json new file mode 100644 index 00000000..96141147 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "location.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location.imageset/location.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location.imageset/location.svg new file mode 100644 index 00000000..2b5e0de1 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location.imageset/location.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/Contents.json deleted file mode 100644 index f0863b1f..00000000 --- a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "location_image.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "location_image 1.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "location_image 2.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 1.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 1.png deleted file mode 100644 index 734c5430..00000000 Binary files a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 1.png and /dev/null differ diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 2.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 2.png deleted file mode 100644 index 734c5430..00000000 Binary files a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 2.png and /dev/null differ diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image.png deleted file mode 100644 index 734c5430..00000000 Binary files a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image.png and /dev/null differ diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workPlaceMarker.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workPlaceMarker.imageset/Contents.json new file mode 100644 index 00000000..3d93cb04 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workPlaceMarker.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "workPlaceMarker.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workPlaceMarker.imageset/workPlaceMarker.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workPlaceMarker.imageset/workPlaceMarker.svg new file mode 100644 index 00000000..7a63d971 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workPlaceMarker.imageset/workPlaceMarker.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerMarker.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerMarker.imageset/Contents.json new file mode 100644 index 00000000..8a7d330d --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerMarker.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "workerMarker.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerMarker.imageset/workerMarker.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerMarker.imageset/workerMarker.svg new file mode 100644 index 00000000..73c96b2d --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerMarker.imageset/workerMarker.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Board/BoardSortigHeaderView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Board/BoardSortigHeaderView.swift new file mode 100644 index 00000000..1e227f32 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Board/BoardSortigHeaderView.swift @@ -0,0 +1,44 @@ +// +// BoardSortigHeaderView.swift +// DSKit +// +// Created by choijunios on 8/15/24. +// + +import UIKit + +public class BoardSortigHeaderView: UIView { + + public let sortingTypeButton: ImageTextButton = { + let button = ImageTextButton( + iconImage: DSKitAsset.Icons.chevronDown.image, + position: .postfix + ) + button.label.textString = "정렬 기준" + button.label.attrTextColor = DSKitAsset.Colors.gray300.color + return button + }() + + public init() { + super.init(frame: .zero) + setLayout() + } + + required init?(coder: NSCoder) { fatalError() } + + func setLayout() { + + [ + sortingTypeButton + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + sortingTypeButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 24), + sortingTypeButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -24), + sortingTypeButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -12), + ]) + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift index 9a2daceb..e637242c 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift @@ -42,7 +42,6 @@ public struct CenterEmployCardRO { applicantCount: 2 ) - public static func create(_ vo: CenterEmployCardVO) -> CenterEmployCardRO { .init( startDay: vo.startDay, @@ -187,7 +186,6 @@ fileprivate class TextVM: CenterEmployCardViewModelable { var terminatePostBtnClicked: RxRelay.PublishRelay = .init() init() { - renderObject = publishObect.asDriver(onErrorJustReturn: .mock) } } diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift index 1d3bbb58..6ae37e2c 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift @@ -10,6 +10,76 @@ import RxSwift import RxCocoa import Entity +public class WorkerEmployCardRO { + + let showBiginnerTag: Bool + let showDayLeftTag: Bool + let dayLeftTagText: String? + let titleText: String + let distanceFromWorkPlace: String + let targetInfoText: String + let workDaysText: String + let workTimeText: String + let payText: String + + init(showBiginnerTag: Bool, showDayLeftTag: Bool, dayLeftTagText: String?, titleText: String, distanceFromWorkPlace: String, targetInfoText: String, workDaysText: String, workTimeText: String, payText: String) { + self.showBiginnerTag = showBiginnerTag + self.showDayLeftTag = showDayLeftTag + self.dayLeftTagText = dayLeftTagText + self.titleText = titleText + self.distanceFromWorkPlace = distanceFromWorkPlace + self.targetInfoText = targetInfoText + self.workDaysText = workDaysText + self.workTimeText = workTimeText + self.payText = payText + } + + public static func create(vo: WorkerEmployCardVO) -> WorkerEmployCardRO { + + var dayLeftTagText: String? = nil + var showDayLeftTag: Bool = false + + if (0...14).contains(vo.dayLeft) { + showDayLeftTag = true + dayLeftTagText = vo.dayLeft == 0 ? "D-Day" : "D-\(vo.dayLeft)" + } + + let targetInfoText = "\(vo.careGrade.textForCellBtn)등급 \(vo.targetAge)세 \(vo.targetGender.twoLetterKoreanWord)" + + let workDaysText = vo.days.sorted(by: { d1, d2 in + d1.rawValue < d2.rawValue + }).map({ $0.korOneLetterText }).joined(separator: ",") + + let workTimeText = "\(vo.startTime) - \(vo.endTime)" + let payText = "\(vo.paymentType.korLetterText) \(vo.paymentAmount) 원" + + return .init( + showBiginnerTag: vo.isBeginnerPossible, + showDayLeftTag: showDayLeftTag, + dayLeftTagText: dayLeftTagText, + titleText: vo.title, + distanceFromWorkPlace: vo.distanceFromWorkPlace, + targetInfoText: targetInfoText, + workDaysText: workDaysText, + workTimeText: workTimeText, + payText: payText + ) + } + + public static let `mock`: WorkerEmployCardRO = .init( + showBiginnerTag: true, + showDayLeftTag: true, + dayLeftTagText: "D-14", + titleText: "사울시 강남동", + distanceFromWorkPlace: "1.1km", + targetInfoText: "1등급 54세 여성", + workDaysText: "", + workTimeText: "월, 화, 수", + payText: "시급 5000원" + ) +} + + public class WorkerEmployCard: UIView { // View @@ -46,7 +116,7 @@ public class WorkerEmployCard: UIView { let label = IdleLabel(typography: .Subtitle2) return label }() - let timeTakenForWalkLabel: IdleLabel = { + let distanceFromWorkPlaceLabel: IdleLabel = { let label = IdleLabel(typography: .Body3) label.attrTextColor = DSKitAsset.Colors.gray500.color return label @@ -68,7 +138,7 @@ public class WorkerEmployCard: UIView { return label }() - let payPerHourLabel: IdleLabel = { + let payLabel: IdleLabel = { let label = IdleLabel(typography: .Body3) label.attrTextColor = DSKitAsset.Colors.gray500.color return label @@ -114,7 +184,7 @@ public class WorkerEmployCard: UIView { let titleStack = HStack( [ titleLabel, - timeTakenForWalkLabel + distanceFromWorkPlaceLabel ], spacing: 8, alignment: .bottom @@ -166,7 +236,7 @@ public class WorkerEmployCard: UIView { payView.backgroundColor = .clear [ payImage, - payPerHourLabel + payLabel ] .forEach { $0.translatesAutoresizingMaskIntoConstraints = false @@ -180,10 +250,10 @@ public class WorkerEmployCard: UIView { payView.leadingAnchor.constraint(equalTo: payImage.leadingAnchor), payView.bottomAnchor.constraint(equalTo: payImage.bottomAnchor), - payPerHourLabel.leadingAnchor.constraint(equalTo: payImage.trailingAnchor, constant: 2), - payPerHourLabel.centerYAnchor.constraint(equalTo: payImage.centerYAnchor), + payLabel.leadingAnchor.constraint(equalTo: payImage.trailingAnchor, constant: 2), + payLabel.centerYAnchor.constraint(equalTo: payImage.centerYAnchor), - payView.trailingAnchor.constraint(equalTo: payPerHourLabel.trailingAnchor), + payView.trailingAnchor.constraint(equalTo: payLabel.trailingAnchor), ]) @@ -224,23 +294,25 @@ public class WorkerEmployCard: UIView { ]) } - public func bind(vo: WorkerEmployCardVO) { - - beginnerTag.isHidden = !vo.isBeginnerPossible - if vo.dayLeft <= 0 { - if vo.dayLeft == 0 { - dayLeftTag.textString = "D-Day" - } else { - dayLeftTag.isHidden = true - } - } else { - dayLeftTag.textString = "D-\(vo.dayLeft)" - } - titleLabel.textString = vo.title - timeTakenForWalkLabel.textString = vo.timeTakenForWalk - serviceTargetInfoLabel.textString = "\(vo.targetLevel)등급 \(vo.targetAge)세 \(vo.targetGender.twoLetterKoreanWord)" - workDaysLabel.textString = vo.days.map({ $0.korOneLetterText }).joined(separator: ",") - workTimeLabel.textString = "\(vo.startTime) - \(vo.endTime)" - payPerHourLabel.textString = "\(vo.paymentType.korLetterText) \(vo.paymentAmount) 원" + public func setToPostAppearance() { + titleLabel.typography = .Subtitle1 + distanceFromWorkPlaceLabel.isHidden = true + serviceTargetInfoLabel.typography = .Body3 + workDaysLabel.typography = .Body2 + workTimeLabel.typography = .Body2 + payLabel.typography = .Body2 + } + + public func bind(ro: WorkerEmployCardRO) { + + beginnerTag.isHidden = !ro.showBiginnerTag + dayLeftTag.isHidden = !ro.showDayLeftTag + dayLeftTag.textString = ro.dayLeftTagText ?? "" + titleLabel.textString = ro.titleText + distanceFromWorkPlaceLabel.textString = ro.distanceFromWorkPlace + serviceTargetInfoLabel.textString = ro.targetInfoText + workDaysLabel.textString = ro.workDaysText + workTimeLabel.textString = ro.workTimeText + payLabel.textString = ro.payText } } diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift index 4805f6ed..4277bc3a 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift @@ -10,24 +10,59 @@ import RxSwift import RxCocoa import Entity +/// WorkerEmployCardCell에서 사용됩니다. +public struct ApplicationInfo { + let isApplied: Bool + let applicationDateText: String + + public static let mock: ApplicationInfo = .init( + isApplied: true, + applicationDateText: "2024. 10. 22" + ) + + public init(isApplied: Bool, applicationDateText: String) { + self.isApplied = isApplied + self.applicationDateText = applicationDateText + } +} + +public protocol WorkerEmployCardViewModelable { + + // Output + var renderObject: Driver? { get } + var applicationInformation: Driver? { get } + + // Input + var cardClicked: PublishRelay { get } + var applyButtonClicked: PublishRelay { get } + + /// true일 경우 즐겨 찾기에 등록됩니다. + var starButtonClicked: PublishRelay { get } +} + public class WorkerEmployCardCell: UITableViewCell { public static let identifier = String(describing: WorkerEmployCardCell.self) - let tappableArea: TappableUIView = .init() - let cardView = WorkerEmployCard() + var viewModel: WorkerEmployCardViewModelable? + private var disposables: [Disposable?]? - let applyButton: TextButtonType1 = { - - let btn = TextButtonType1( - labelText: "지원하기" - ) + public override func layoutSubviews() { + super.layoutSubviews() + + contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 8, right: 20)) + } + + // View + let tappableArea: TappableUIView = .init() + let cardView = WorkerEmployCard() + let applyButton: IdlePrimaryCardButton = { + let btn = IdlePrimaryCardButton(level: .large) + btn.label.textString = "" return btn }() - private var touchDispoable: Disposable? - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setAppearance() @@ -36,8 +71,10 @@ public class WorkerEmployCardCell: UITableViewCell { public required init?(coder: NSCoder) { fatalError() } public override func prepareForReuse() { - touchDispoable?.dispose() - touchDispoable = nil + viewModel = nil + + disposables?.forEach { $0?.dispose() } + disposables = nil } func setAppearance() { @@ -77,10 +114,47 @@ public class WorkerEmployCardCell: UITableViewCell { ]) } - public func bind(vo: WorkerEmployCardVO) { + public func bind(viewModel: WorkerEmployCardViewModelable) { + + self.viewModel = viewModel - // tap설정 + // input + let disposables: [Disposable?] = [ + // Output + viewModel + .applicationInformation? + .drive(onNext: { [weak self] info in + guard let self else { return } + if info.isApplied { + applyButton.setEnabled(false) + applyButton.label.textString = "지원완료 \(info.applicationDateText)" + } else { + applyButton.setEnabled(true) + applyButton.label.textString = "지원하기" + } + }), + viewModel + .renderObject? + .drive(onNext: { [cardView] ro in + cardView.bind(ro: ro) + }), + + // Input + tappableArea + .rx.tap + .bind(to: viewModel.cardClicked), + + applyButton + .rx.tap + .bind(to: viewModel.applyButtonClicked), + + cardView + .starButton + .eventPublisher + .map { $0 == .accent } + .bind(to: viewModel.starButtonClicked), + ] - cardView.bind(vo: vo) + self.disposables = disposables } } diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift index ea9b61d7..1f6422f5 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift @@ -47,7 +47,8 @@ public class CenterInfoCardView: TappableUIView { private func setLayout() { - let locationImageView = DSKitAsset.Icons.locationSmall.image.toView() + let locationImageView = DSKitAsset.Icons.location.image.toView() + locationImageView.tintColor = DSColor.gray400.color let locationStack = HStack( [ locationImageView, diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/ProfileButton/CenterProfileButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/ProfileButton/CenterProfileButton.swift index 9578dbd7..4c4fca1a 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/ProfileButton/CenterProfileButton.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/ProfileButton/CenterProfileButton.swift @@ -65,7 +65,7 @@ public class CenterProfileButton: TappableUIView { private func setLayout() { - let locImage = DSKitAsset.Icons.locationSmall.image.toView() + let locImage = DSKitAsset.Icons.location.image.toView() let locationStack = HStack( [ locImage, diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/TabBarContainer/IdleTabBarItem.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/TabBarContainer/IdleTabBarItem.swift index 8ebb488a..e03a4ad7 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/TabBarContainer/IdleTabBarItem.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/TabBarContainer/IdleTabBarItem.swift @@ -23,15 +23,15 @@ public class IdleTabBarItem: TappableUIView { // idle let idleIconColor: UIColor = DSColor.gray300.color - + let idleTextColor: UIColor = DSColor.gray300.color // accent let accentIconColor: UIColor = DSColor.gray700.color + let accentTextColor: UIColor = DSColor.gray700.color // View let label: IdleLabel = { let label = IdleLabel(typography: .caption) - label.attrTextColor = DSColor.gray700.color return label }() let imageView: UIImageView = { @@ -54,6 +54,7 @@ public class IdleTabBarItem: TappableUIView { private func setAppearance() { imageView.tintColor = idleIconColor + label.attrTextColor = idleTextColor } private func setLayout() { @@ -86,10 +87,12 @@ public class IdleTabBarItem: TappableUIView { private func setToIdle() { imageView.tintColor = idleIconColor + label.attrTextColor = idleTextColor } private func setToAccent() { imageView.tintColor = accentIconColor + label.attrTextColor = accentTextColor } } diff --git a/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift b/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift index afac6eae..83ce7b81 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift @@ -16,7 +16,7 @@ public extension UIImageView { }() static let locationMark: UIImageView = { - let view = UIImageView(image: DSKitAsset.Icons.locationSmall.image) + let view = UIImageView(image: DSKitAsset.Icons.location.image) view.contentMode = .scaleAspectFit return view }() diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Typography/Typograpy.swift b/project/Projects/Presentation/DSKit/Sources/Component/Typography/Typograpy.swift index 2d0a02bc..9daf8d98 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/Typography/Typograpy.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Typography/Typograpy.swift @@ -32,6 +32,7 @@ public enum Typography { case Body3 case caption + case caption2 var lineHeight: CGFloat { switch self { @@ -59,6 +60,8 @@ public enum Typography { 20 case .caption: 18.6 + case .caption2: + 18.6 } } @@ -149,6 +152,13 @@ public enum Typography { letterSpacing: -0.2, color: DSKitAsset.Colors.gray900.color ) + case .caption2: + Self.createAttribute( + weight: .Semibold, + size: 12, + letterSpacing: -0.2, + color: DSKitAsset.Colors.gray900.color + ) } } diff --git a/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/SceneDelegate.swift index 2c9f771e..7c3560ed 100644 --- a/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Base/ExampleApp/Sources/SceneDelegate.swift @@ -16,12 +16,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = scene as? UIWindowScene else { return } - let vc = PostDetailVC() - window = UIWindow(windowScene: windowScene) - window?.rootViewController = vc - - vc.bind() + window?.rootViewController = UIViewController() window?.makeKeyAndVisible() } diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/AddressContentView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/AddressContentView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/AddressContentView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/AddressContentView.swift 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/Center/Edit/ApplicationDetailViewContentView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/ApplicationDetailViewContentView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/ApplicationDetailViewContentView.swift 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/Center/Edit/CustomerInformationContentView.swift similarity index 99% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerInformationContentView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/CustomerInformationContentView.swift index cff54e8b..0eb8a1a1 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerInformationContentView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/CustomerInformationContentView.swift @@ -249,7 +249,7 @@ public class CustomerInformationContentView: UIView { birthYearField.textString = stateFromVM.birthYear // 몸무게 - weightField.textField.textString = stateFromVM.weight.emptyDefault("-") + weightField.textField.textString = stateFromVM.weight.emptyDefault("") // 요양등급 if let state = stateFromVM.careGrade { diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerRequirementContentView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/CustomerRequirementContentView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/CustomerRequirementContentView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/CustomerRequirementContentView.swift diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/WorkTimeAndPayContentView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/WorkTimeAndPayContentView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Edit/WorkTimeAndPayContentView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/Edit/WorkTimeAndPayContentView.swift 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/Center/OverView/ApplicationDetailDisplayingView.swift similarity index 78% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/ApplicationDetailDisplayingView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/ApplicationDetailDisplayingView.swift index 19811692..5f1311c7 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/ApplicationDetailDisplayingView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/ApplicationDetailDisplayingView.swift @@ -105,6 +105,36 @@ public class ApplicationDetailDisplayingView: HStack { public extension ApplicationDetailDisplayingView { + private func applyObject(_ object: ApplicationDetailStateObject) { + expPreferenceLabel.textString = object.experiencePreferenceType?.korTextForBtn ?? "오류" + + applTypeLabel.textString = object.applyType.compactMap({ (key, value) -> String? in + value ? key.twoLetterKorTextForDisplay : nil + }).joined(separator: ", ") + + if let type = object.applyDeadlineType { + if type == .specificDate { + if let date = object.deadlineDate { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy. MM. dd" + let deadLineText = dateFormatter.string(from: date) + deadlineLabel.textString = deadLineText + } else { + deadlineLabel.textString = "오류" + } + } else { + deadlineLabel.textString = type.korTextForBtn + } + + } else { + deadlineLabel.textString = "오류" + } + } + + func bind(applicationDetailStateObject: ApplicationDetailStateObject) { + applyObject(applicationDetailStateObject) + } + /// ViewModelType: ApplicationDetailContentVMable func bind(viewModel: ApplicationDetailDisplayingVMable) { @@ -112,30 +142,7 @@ public extension ApplicationDetailDisplayingView { .casting_applicationDetail .drive(onNext: { [weak self] object in guard let self else { return } - - expPreferenceLabel.textString = object.experiencePreferenceType?.korTextForBtn ?? "오류" - - applTypeLabel.textString = object.applyType.compactMap({ (key, value) -> String? in - value ? key.twoLetterKorTextForDisplay : nil - }).joined(separator: ", ") - - if let type = object.applyDeadlineType { - if type == .specificDate { - if let date = object.deadlineDate { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy. MM. dd" - let deadLineText = dateFormatter.string(from: date) - deadlineLabel.textString = deadLineText - } else { - deadlineLabel.textString = "오류" - } - } else { - deadlineLabel.textString = type.korTextForBtn - } - - } else { - deadlineLabel.textString = "오류" - } + applyObject(object) }) .disposed(by: disposeBag) } 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/Center/OverView/CustomerInformationDisplayingView.swift similarity index 81% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/CustomerInformationDisplayingView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/CustomerInformationDisplayingView.swift index 64c86890..a59cec74 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/CustomerInformationDisplayingView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/CustomerInformationDisplayingView.swift @@ -202,32 +202,57 @@ public class CustomerInformationDisplayingView: VStack { public extension CustomerInformationDisplayingView { + private func applyObject(_ object: CustomerInformationStateObject) { + nameLabel.textString = object.name + genderLabel.textString = object.gender?.twoLetterKoreanWord ?? "오류" + birthYearLabel.textString = object.birthYear + weightLabel.textString = object.weight + + if let careGrade = object.careGrade { + let text: String = careGrade.textForCellBtn + "등급" + careGradeLabel.textString = text + } else { + careGradeLabel.textString = "오류" + } + + if let cognitionState = object.cognitionState { + cognitionStateLabel.textString = cognitionState.korTextForCellBtn + } else { + cognitionStateLabel.textString = "오류" + } + + deceaseLabel.textString = object.deceaseDescription.isEmpty ? "-" : object.deceaseDescription + } + private func applyObject(_ object: CustomerRequirementStateObject) { + + mealSupportLabel.textString = object.mealSupportNeeded == true ? "필요" : "불필요" + toiletSupportLabel.textString = object.toiletSupportNeeded == true ? "필요" : "불필요" + movingSupportLabel.textString = object.movingSupportNeeded == true ? "필요" : "불필요" + + let dailySupportText = object.dailySupportTypeNeeds.compactMap { (day, isActive) -> String? in + return isActive ? day.korLetterTextForBtn : nil + }.joined(separator: ", ") + + dailySupportLabel.textString = dailySupportText + + additionalTextLabel.textString = object.additionalRequirement + } + + func bind( + customerInformationStateObject: CustomerInformationStateObject, + customerRequirementStateObject: CustomerRequirementStateObject + ) { + applyObject(customerInformationStateObject) + applyObject(customerRequirementStateObject) + } + func bind(viewModel: CustomerInformationDisplayingVMable) { viewModel .casting_customerInformation .drive(onNext: { [weak self] object in guard let self else { return } - - nameLabel.textString = object.name - genderLabel.textString = object.gender?.twoLetterKoreanWord ?? "오류" - birthYearLabel.textString = object.birthYear - weightLabel.textString = object.weight - - if let careGrade = object.careGrade { - let text: String = careGrade.textForCellBtn + "등급" - careGradeLabel.textString = text - } else { - careGradeLabel.textString = "오류" - } - - if let cognitionState = object.cognitionState { - cognitionStateLabel.textString = cognitionState.korTextForCellBtn - } else { - cognitionStateLabel.textString = "오류" - } - - deceaseLabel.textString = object.deceaseDescription.isEmpty ? "-" : object.deceaseDescription + applyObject(object) }) .disposed(by: disposeBag) @@ -235,18 +260,7 @@ public extension CustomerInformationDisplayingView { .casting_customerRequirement .drive(onNext: { [weak self] object in guard let self else { return } - - mealSupportLabel.textString = object.mealSupportNeeded == true ? "필요" : "불필요" - toiletSupportLabel.textString = object.toiletSupportNeeded == true ? "필요" : "불필요" - movingSupportLabel.textString = object.movingSupportNeeded == true ? "필요" : "불필요" - - let dailySupportText = object.dailySupportTypeNeeds.compactMap { (day, isActive) -> String? in - return isActive ? day.korLetterTextForBtn : nil - }.joined(separator: ", ") - - dailySupportLabel.textString = dailySupportText - - additionalTextLabel.textString = object.additionalRequirement + applyObject(object) }) .disposed(by: disposeBag) } 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/Center/OverView/WorkConditionDisplayingView.swift similarity index 76% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/WorkConditionDisplayingView.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/WorkConditionDisplayingView.swift index 5569d4cc..d4f35283 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/OverView/WorkConditionDisplayingView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/WorkConditionDisplayingView.swift @@ -108,30 +108,45 @@ public class WorkConditionDisplayingView: HStack { public extension WorkConditionDisplayingView { + private func applyObject(_ object: WorkTimeAndPayStateObject) { + let daysText = object.selectedDays.compactMap { (day, isActive) -> String? in + return isActive ? day.korOneLetterText : nil + }.joined(separator: ", ") + + workDaysLabel.textString = daysText + + let workTimeText = [ + object.workStartTime?.convertToStringForButton() ?? "00:00", + object.workEndTime?.convertToStringForButton() ?? "00:00" + ].joined(separator: " - ") + + workTimeLabel.textString = workTimeText + + let paymentTypeText = object.paymentType?.korLetterText ?? "오류" + let paymentAmountText = object.paymentAmount + + workPaymentLabel.textString = "\(paymentTypeText) \(paymentAmountText)원" + } + + private func applyObject(_ object: AddressInputStateObject) { + workLocationLabel.textString = object.addressInfo?.roadAddress ?? "오류" + } + + func bind( + workTimeAndPayStateObject: WorkTimeAndPayStateObject, + addressInputStateObject: AddressInputStateObject + ) { + applyObject(workTimeAndPayStateObject) + applyObject(addressInputStateObject) + } + func bind(viewModel: WorkConditionDisplayingVMable) { viewModel .casting_workTimeAndPay .drive(onNext: { [weak self] object in guard let self else { return } - - let daysText = object.selectedDays.compactMap { (day, isActive) -> String? in - return isActive ? day.korOneLetterText : nil - }.joined(separator: ", ") - - workDaysLabel.textString = daysText - - let workTimeText = [ - object.workStartTime?.convertToStringForButton() ?? "00:00", - object.workEndTime?.convertToStringForButton() ?? "00:00" - ].joined(separator: " - ") - - workTimeLabel.textString = workTimeText - - let paymentTypeText = object.paymentType?.korLetterText ?? "오류" - let paymentAmountText = object.paymentAmount - - workPaymentLabel.textString = "\(paymentTypeText) \(paymentAmountText)원" + applyObject(object) }) .disposed(by: disposeBag) @@ -139,8 +154,7 @@ public extension WorkConditionDisplayingView { .casting_addressInput .drive(onNext: { [weak self] object in guard let self else { return } - - workLocationLabel.textString = object.addressInfo?.roadAddress ?? "오류" + applyObject(object) }) .disposed(by: disposeBag) } diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/WorkLocationView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/WorkLocationView.swift deleted file mode 100644 index c951fd38..00000000 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/WorkLocationView.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// WorkLocationView.swift -// BaseFeature -// -// Created by choijunios on 8/7/24. -// - -import UIKit -import PresentationCore -import RxCocoa -import RxSwift -import Entity -import DSKit -import NMapsMap - -public class WorkLocationView: VStack { - - // Init - - // View - let walkToLocationLabel: UILabel = { - let label = UILabel() - label.text = "거주지에서--" - return label - }() - - let timeCostByWalkLabel: IdleLabel = { - let label = IdleLabel(typography: .Subtitle2) - label.textString = "걸어서 ~ 소요" - label.textAlignment = .left - return label - }() - - let mapView: NMFNaverMapView = { - - let view = NMFNaverMapView(frame: .zero) - view.layer.cornerRadius = 8 - view.clipsToBounds = true - return view - }() - - // Observable - private let disposeBag = DisposeBag() - - public init() { - super.init([], spacing: 16, alignment: .fill) - setAppearance() - setLayout() - } - - public required init(coder: NSCoder) { fatalError() } - - private func setAppearance() { - - } - - private func setLayout() { - - let walkingImage = DSKitAsset.Icons.walkingHuman.image.toView() - let timeCostStack = HStack([walkingImage, timeCostByWalkLabel], spacing: 6, alignment: .center) - - let labelStack = VStack([walkToLocationLabel, timeCostStack], spacing: 4, alignment: .leading) - - [ - labelStack, - mapView - ].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - self.addArrangedSubview($0) - } - - NSLayoutConstraint.activate([ - mapView.heightAnchor.constraint(equalToConstant: 224), - ]) - } - - private func setLocationLabel(roadAddress: String) { - let text = "거주지에서 \(roadAddress) 까지" - var normalAttr = Typography.Body2.attributes - normalAttr[.foregroundColor] = DSKitAsset.Colors.gray500.color - - let attrText = NSMutableAttributedString(string: text, attributes: normalAttr) - - let roadTextFont = Typography.Subtitle3.attributes[.font]! - - let range = NSRange(text.range(of: roadAddress)!, in: text) - attrText.addAttribute(.font, value: roadTextFont, range: range) - - walkToLocationLabel.attributedText = attrText - } - - private func configureMapAppearance() { - - mapView.mapView.touchDelegate = self - - let initialCoordinate = NMGLatLng(lat: 37.5666102, lng: 126.9783881) - let cameraPosition = NMFCameraPosition(initialCoordinate, zoom: 15.0) - - mapView.mapView.moveCamera(NMFCameraUpdate(position: cameraPosition)) - mapView.showZoomControls = false - } - - private func setObservable() { - - } - - public func bind() { - configureMapAppearance() - } -} - -extension WorkLocationView: NMFMapViewTouchDelegate { - public func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { - - printIfDebug("\(latlng.lat), \(latlng.lng)") - } -} diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/Coordinator/PostDetailForWorkerCoodinator.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/Coordinator/PostDetailForWorkerCoodinator.swift new file mode 100644 index 00000000..ee198bc6 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/Coordinator/PostDetailForWorkerCoodinator.swift @@ -0,0 +1,72 @@ +// +// Coordinator.swift +// BaseFeature +// +// Created by choijunios on 8/15/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class PostDetailForWorkerCoodinator: ChildCoordinator { + + public struct Dependency { + let postId: String + weak var parent: WorkerRecruitmentBoardCoordinatable? + let navigationController: UINavigationController + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(postId: String, parent: WorkerRecruitmentBoardCoordinatable? = nil, navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.postId = postId + self.parent = parent + self.navigationController = navigationController + self.recruitmentPostUseCase = recruitmentPostUseCase + } + } + + public weak var viewControllerRef: UIViewController? + public weak var parent: WorkerRecruitmentBoardCoordinatable? + + let postId: String + public let navigationController: UINavigationController + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init( + dependency: Dependency + ) { + self.postId = dependency.postId + self.parent = dependency.parent + self.navigationController = dependency.navigationController + self.recruitmentPostUseCase = dependency.recruitmentPostUseCase + } + + deinit { + printIfDebug("\(String(describing: PostDetailForWorkerCoodinator.self))") + } + + public func start() { + let vc = PostDetailForWorkerVC() + let vm = PostDetailForWorkerVM( + postId: postId, + coordinator: self, + recruitmentPostUseCase: recruitmentPostUseCase + ) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } +} + +extension PostDetailForWorkerCoodinator { + + func showCenterProfileScreen(centerId: String) { + parent?.showCenterProfile(centerId: centerId) + } +} diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/PostDetailVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/PostDetailForWorkerVC.swift similarity index 59% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/PostDetailVC.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/PostDetailForWorkerVC.swift index 1f77f7b0..187c09a2 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/PostDetailVC.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/PostDetailForWorkerVC.swift @@ -1,5 +1,5 @@ // -// PostDetailVc.swift +// PostDetailForWorkerVC.swift // BaseFeature // // Created by choijunios on 8/7/24. @@ -12,17 +12,21 @@ import RxSwift import Entity import DSKit -public class PostDetailVC: BaseViewController { +/// 센토도 요양보호사가 보는 공고화면을 볼 수 있기 때문에 해당뷰를 BaseFeature에 구현하였습니다. +public class PostDetailForWorkerVC: BaseViewController { + + var viewModel: PostDetailForWorkerViewModelable? // Init // View - let navigationBar: NavigationBarType1 = { - let bar = NavigationBarType1(navigationTitle: "공고 정보") + let navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar(innerViews: []) + bar.titleLabel.textString = "공고 정보" return bar }() - let contentView = PostDetailContentView() + let contentView = PostDetailForWorkerContentView() // 하단 버튼 let csButton: IdleSecondaryButton = { @@ -93,8 +97,8 @@ public class PostDetailVC: BaseViewController { } NSLayoutConstraint.activate([ - navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 21), - navigationBar.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 12), + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationBar.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), navigationBar.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), scrollView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor), @@ -120,26 +124,140 @@ public class PostDetailVC: BaseViewController { .disposed(by: disposeBag) } - public func bind() { + public func bind(viewModel: PostDetailForWorkerViewModelable) { + + self.viewModel = viewModel + + // Output + viewModel + .postForWorkerBundle? + .drive(onNext: { + [weak self] bundle in + guard let self else { return } + + // 상단 구인공고 카드 + contentView.cardView.bind( + ro: WorkerEmployCardRO.create( + vo: .create( + workTimeAndPay: bundle.workTimeAndPay, + customerRequirement: bundle.customerRequirement, + customerInformation: bundle.customerInformation, + applicationDetail: bundle.applicationDetail, + addressInfo: bundle.addressInfo + ) + ) + ) + + // 근무 조건 + contentView.workConditionView.bind( + workTimeAndPayStateObject: bundle.workTimeAndPay, + addressInputStateObject: bundle.addressInfo + ) + + // 고객 정보 + contentView.customerInfoView.bind( + customerInformationStateObject: bundle.customerInformation, + customerRequirementStateObject: bundle.customerRequirement + ) + + // 추가 지원 정보 + contentView.applicationDetailView.bind( + applicationDetailStateObject: bundle.applicationDetail + ) + + // 센터 정보 카드 + let centerInfo = bundle.centerInfo + contentView.centerInfoCard.bind( + nameText: centerInfo.centerName, + locationText: centerInfo.centerRoadAddress + ) + + }) + .disposed(by: disposeBag) + + if let locationInfo = viewModel.locationInfo?.asObservable().share() { + + locationInfo + .subscribe(onNext: { + [weak self] info in + // 위치정보 전달 + self?.contentView.workPlaceAndWorkerLocationView.bind(locationRO: info) + }) + .disposed(by: disposeBag) + + // 지도화면 클릭시 + contentView.workPlaceAndWorkerLocationView.mapViewBackGround + .rx.tap + .withLatestFrom(locationInfo) + .subscribe { [weak self] locationInfo in + let fullMapVC = WorkPlaceAndWorkerLocationFullVC() + fullMapVC.bind(locationRO: locationInfo) + self?.navigationController?.pushViewController(fullMapVC, animated: true) + } + .disposed(by: disposeBag) + } + + viewModel + .alert? + .drive(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + + // Input + + // viewWillAppear + self.rx.viewWillAppear + .map({ _ in }) + .bind(to: viewModel.viewWillAppear) + .disposed(by: disposeBag) - // back button + // 지원하기 + applyButton + .rx.tap + .bind(to: viewModel.applyButtonClicked) + .disposed(by: disposeBag) + + // 즐겨 찾기 버튼 + contentView.cardView.starButton + .eventPublisher + .map { state in return state == .accent } + .bind(to: viewModel.startButtonClicked) + .disposed(by: disposeBag) - // Content view - contentView.bind() + // 센터 프로필 보기 버튼 + contentView.centerInfoCard + .rx.tap + .bind(to: viewModel.centerCardClicked) + .disposed(by: disposeBag) - // button + // 뒤로가기 버튼 + navigationBar.backButton + .rx.tap + .bind(to: viewModel.backButtonClicked) + .disposed(by: disposeBag) } } // MARK: PostDetailContentView -public class PostDetailContentView: UIView { +public class PostDetailForWorkerContentView: UIView { + + /// 구인공고 카드 + let cardView: WorkerEmployCard = { + let view = WorkerEmployCard() + view.setToPostAppearance() + return view + }() - let cardView: WorkerEmployCard = .init() + /// 지도뷰 + public let workPlaceAndWorkerLocationView = WorkPlaceAndWorkerLocationView() - let workLocationView = WorkLocationView() + /// 공고 상세정보들 let workConditionView = WorkConditionDisplayingView() let customerInfoView = CustomerInformationDisplayingView() let applicationDetailView = ApplicationDetailDisplayingView() + + /// 센터 프로필로 이동하는 카드및 센터정보 표시 let centerInfoCard = CenterInfoCardView() public init() { @@ -159,7 +277,7 @@ public class PostDetailContentView: UIView { func setLayout() { let titleViewData: [(title: String, view: UIView)] = [ - ("근무 장소", workLocationView), + ("근무 장소", workPlaceAndWorkerLocationView), ("근무 조건", workConditionView), ("고객 정보", customerInfoView), ("추가 지원 정보", applicationDetailView), @@ -237,11 +355,4 @@ public class PostDetailContentView: UIView { } - - public func bind() { - - cardView.bind(vo: .mock) - workLocationView.bind() - centerInfoCard.bind(nameText: "세얼간이 센터", locationText: "아남타워 7층") - } } diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/SelectCSTypeVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/SelectCSTypeVC.swift similarity index 100% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Detail/SelectCSTypeVC.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/SelectCSTypeVC.swift diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/WorkPlaceAndWorkerLocationFullVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/WorkPlaceAndWorkerLocationFullVC.swift new file mode 100644 index 00000000..32026fa3 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/WorkPlaceAndWorkerLocationFullVC.swift @@ -0,0 +1,104 @@ +// +// WorkPlaceAndWorkerLocationFullVC.swift +// BaseFeature +// +// Created by choijunios on 8/16/24. +// + +import UIKit +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import NMapsMap + +public class WorkPlaceAndWorkerLocationFullVC: BaseViewController { + + // Init + + // View + let navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar( + titleText: "", + innerViews: [] + ) + return bar + }() + + let mapView: NMFNaverMapView = { + let view = NMFNaverMapView(frame: .zero) + view.backgroundColor = DSColor.gray050.color + view.layer.cornerRadius = 8 + view.clipsToBounds = true + return view + }() + + // 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 = DSColor.gray0.color + } + + private func setLayout() { + [ + navigationBar, + mapView, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationBar.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + navigationBar.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), + + mapView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor), + mapView.leftAnchor.constraint(equalTo: view.leftAnchor), + mapView.rightAnchor.constraint(equalTo: view.rightAnchor), + mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + private func setObservable() { + navigationBar.backButton.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.navigationController?.popViewController(animated: true) + }) + .disposed(by: disposeBag) + } + + public func bind(locationRO: WorkPlaceAndWorkerLocationMapRO) { + + navigationBar.titleLabel.textString = locationRO.workPlaceRoadAddress + + mapView.bind( + locationRO: locationRO, + paddingInsets: .init( + top: 0, + left: 71, + bottom: 0, + right: 71 + ) + ) + + // 지도 뷰 Config + mapView.showLocationButton = true + mapView.showZoomControls = false + } +} + diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/WorkPlaceAndWorkerLocationView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/WorkPlaceAndWorkerLocationView.swift new file mode 100644 index 00000000..23e74079 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/WorkPlaceAndWorkerLocationView.swift @@ -0,0 +1,220 @@ +// +// WorkPlaceAndWorkerLocationView.swift +// BaseFeature +// +// Created by choijunios on 8/7/24. +// + +import UIKit +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import NMapsMap + +public struct WorkPlaceAndWorkerLocationMapRO { + + let workPlaceRoadAddress: String + let homeToworkPlaceText: NSMutableAttributedString + let distanceToWorkPlaceText: String + + let workPlaceLocation: LocationInformation + let workerLocation: LocationInformation? +} + +public class WorkPlaceAndWorkerLocationView: VStack { + + // Init + + // View + let walkToLocationLabel: UILabel = { + let label = UILabel() + return label + }() + + let distanceLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle2) + label.textString = "" + label.textAlignment = .left + return label + }() + + public let mapViewBackGround: TappableUIView = { + let view = TappableUIView() + view.backgroundColor = DSColor.gray050.color + return view + }() + let mapView: NMFNaverMapView = { + let view = NMFNaverMapView(frame: .zero) + view.backgroundColor = DSColor.gray050.color + view.layer.cornerRadius = 8 + view.clipsToBounds = true + view.isUserInteractionEnabled = false + return view + }() + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init([], spacing: 16, alignment: .fill) + setAppearance() + setLayout() + setObservable() + } + + public required init(coder: NSCoder) { fatalError() } + + private func setAppearance() { + + } + + private func setLayout() { + + let walkingImage = DSKitAsset.Icons.walkingHuman.image.toView() + let timeCostStack = HStack([walkingImage, distanceLabel], spacing: 6, alignment: .center) + + let labelStack = VStack([walkToLocationLabel, timeCostStack], spacing: 4, alignment: .leading) + + mapViewBackGround.addSubview(mapView) + mapView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + mapView.topAnchor.constraint(equalTo: mapViewBackGround.topAnchor), + mapView.leftAnchor.constraint(equalTo: mapViewBackGround.leftAnchor), + mapView.rightAnchor.constraint(equalTo: mapViewBackGround.rightAnchor), + mapView.bottomAnchor.constraint(equalTo: mapViewBackGround.bottomAnchor), + ]) + + [ + labelStack, + mapViewBackGround + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addArrangedSubview($0) + } + + NSLayoutConstraint.activate([ + mapViewBackGround.heightAnchor.constraint(equalToConstant: 224), + ]) + } + + private func setObservable() { + + } + + public func bind(locationRO: WorkPlaceAndWorkerLocationMapRO) { + + walkToLocationLabel.attributedText = locationRO.homeToworkPlaceText + distanceLabel.textString = locationRO.distanceToWorkPlaceText + + mapView.bind( + locationRO: locationRO, + paddingInsets: .init( + top: 42, + left: 59, + bottom: 44, + right: 59 + ) + ) + + // - 제스처 끄기 + mapView.mapView.isScrollGestureEnabled = false + mapView.mapView.isZoomGestureEnabled = false + mapView.mapView.isTiltGestureEnabled = false + mapView.mapView.isRotateGestureEnabled = false + mapView.mapView.isStopGestureEnabled = false + + // 지도 뷰 Config + mapView.showCompass = false + mapView.showScaleBar = false + mapView.showZoomControls = false + mapView.showLocationButton = false + } +} + + +extension NMFNaverMapView { + + func bind( + locationRO: WorkPlaceAndWorkerLocationMapRO, + paddingInsets: UIEdgeInsets + ) { + // 마커 설정 + let workPlacePos: NMGLatLng = .init( + lat: locationRO.workPlaceLocation.latitude, + lng: locationRO.workPlaceLocation.longitude + ) + + var posArr = [workPlacePos] + + var workerPos: NMGLatLng? + + if let workerLocation = locationRO.workerLocation { + workerPos = .init( + lat: workerLocation.latitude, + lng: workerLocation.longitude + ) + posArr.append(workerPos!) + } + + let workPlaceMarker = NMFMarker( + position: workPlacePos, + iconImage: .init(image: DSIcon.workPlaceMarker.image) + ) + workPlaceMarker.width = 41 + workPlaceMarker.height = 56 + + var markerArr = [workPlaceMarker] + + var workerMarker: NMFMarker? + + if let workerPos { + workerMarker = .init( + position: workerPos, + iconImage: .init(image: DSIcon.workerMarker.image) + ) + workerMarker?.width = 33 + workerMarker?.height = 44 + + markerArr.append(workerMarker!) + } + + + markerArr.forEach { marker in + marker.mapView = self.mapView + marker.globalZIndex = 40001 + marker.anchor = .init(x: 0.5, y: 1) + } + + // 경로선 + if posArr.count == 2 { + let pathOverlay = NMFPath() + pathOverlay.path = .init(points: posArr) + pathOverlay.width = 3 + pathOverlay.outlineWidth = 0 + pathOverlay.color = DSColor.orange400.color + pathOverlay.mapView = self.mapView + pathOverlay.globalZIndex = 40001 + pathOverlay.zIndex = 0 + } + // 근무지가 우선 표시도되도록 + workPlaceMarker.zIndex = 2 + workerMarker?.zIndex = 1 + + + // 카메라 이동 + let camerUpdate = NMFCameraUpdate( + fit: .init( + latLngs: posArr + ), + paddingInsets: paddingInsets + ) + self.mapView.moveCamera(camerUpdate) + // 지도 Config + let map = self.mapView + map.mapType = .basic + map.symbolScale = 2 + } +} diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/PostDetailForWorkerVM.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/PostDetailForWorkerVM.swift new file mode 100644 index 00000000..ee16d246 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/PostDetailForWorkerVM.swift @@ -0,0 +1,153 @@ +// +// asd.swift +// BaseFeature +// +// Created by choijunios on 8/15/24. +// + +import UIKit +import RxCocoa +import RxSwift +import Entity +import PresentationCore +import UseCaseInterface +import DSKit + +public protocol PostDetailForWorkerViewModelable { + + // Output + var postForWorkerBundle: Driver? { get } + var locationInfo: Driver? { get } + var alert: Driver? { get } + + // Input + var viewWillAppear: PublishRelay { get } + + var backButtonClicked: PublishRelay { get } + var applyButtonClicked: PublishRelay { get } + var startButtonClicked: PublishRelay { get } + var centerCardClicked: PublishRelay { get } +} + +public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { + + public weak var coordinator: PostDetailForWorkerCoodinator? + + // Init + private let postId: String + private let recruitmentPostUseCase: RecruitmentPostUseCase + + + public var postForWorkerBundle: RxCocoa.Driver? + public var locationInfo: RxCocoa.Driver? + + public var alert: RxCocoa.Driver? + + + public var backButtonClicked: RxRelay.PublishRelay = .init() + public var applyButtonClicked: RxRelay.PublishRelay = .init() + public var startButtonClicked: RxRelay.PublishRelay = .init() + public var centerCardClicked: RxRelay.PublishRelay = .init() + public var viewWillAppear: RxRelay.PublishRelay = .init() + + private let disposeBag = DisposeBag() + + public init( + postId: String, + coordinator: PostDetailForWorkerCoodinator?, + recruitmentPostUseCase: RecruitmentPostUseCase + ) + { + self.postId = postId + self.coordinator = coordinator + self.recruitmentPostUseCase = recruitmentPostUseCase + + let getPostDetailResult = viewWillAppear + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase + .getPostDetailForWorker(id: postId) + } + .share() + + let getPostDetailSuccess = getPostDetailResult.compactMap { $0.value } + let getPostDetailFailure = getPostDetailResult.compactMap { $0.error } + + postForWorkerBundle = getPostDetailSuccess.asDriver(onErrorRecover: { _ in fatalError() }) + + // MARK: 센터, 워커 위치정보 + locationInfo = getPostDetailSuccess + .map { [weak self] bundle in + // 요양보호사 위치 가져오기 + let workerLocation = self?.getWorkerLocation() + + let workPlaceLocation = bundle.jobLocation + + let roadAddress = bundle.addressInfo.addressInfo?.roadAddress ?? "근무지 위치" + let text = "거주지에서 \(roadAddress) 까지" + var normalAttr = Typography.Body2.attributes + normalAttr[.foregroundColor] = DSKitAsset.Colors.gray500.color + + let attrText = NSMutableAttributedString(string: text, attributes: normalAttr) + + let roadTextFont = Typography.Subtitle3.attributes[.font]! + + let range = NSRange(text.range(of: roadAddress)!, in: text) + attrText.addAttribute(.font, value: roadTextFont, range: range) + + return WorkPlaceAndWorkerLocationMapRO( + workPlaceRoadAddress: roadAddress, + homeToworkPlaceText: attrText, + distanceToWorkPlaceText: "\(bundle.distanceToWorkPlace)m", + workPlaceLocation: workPlaceLocation, + workerLocation: workerLocation + ) + } + .asDriver(onErrorRecover: { _ in fatalError() }) + + // Alert 처리 필요 + alert = getPostDetailFailure + .map { error in + AlertWithCompletionVO( + title: "공고 불러오기 실패", + message: error.message, + buttonInfo: [ + ("닫기", { [weak self] in + self?.coordinator?.coordinatorDidFinish() + }) + ] + ) + } + .asDriver(onErrorJustReturn: .default) + + + // MARK: 버튼 처리 + backButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + self.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + // 지원하기 버튼 클릭 + + // 즐겨찾기 버튼 클릭 + + // 센터 프로필 조회 버튼클릭 + centerCardClicked + .withLatestFrom(getPostDetailSuccess) + .subscribe(onNext: { [weak self] bundle in + guard let self else { return } + let centerId = bundle.centerInfo.centerId + self.coordinator?.showCenterProfileScreen(centerId: centerId) + }) + .disposed(by: disposeBag) + } + + // MARK: Test + func getWorkerLocation() -> LocationInformation { + return .init( + longitude: 127.046425, + latitude: 37.504588 + ) + } +} diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift index 10508e43..9e4810ca 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift @@ -21,4 +21,16 @@ public extension BaseViewController { alret.addAction(close) present(alret, animated: true, completion: nil) } + + func showAlert(vo: AlertWithCompletionVO) { + let alret = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) + + vo.buttonInfo.forEach { (buttonTitle: String, completion: AlertWithCompletionVO.AlertCompletion?) in + let button = UIAlertAction(title: buttonTitle, style: .default) { _ in + completion?() + } + alret.addAction(button) + } + present(alret, animated: true, completion: nil) + } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/CenterProfileCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/CenterProfileCoordinator.swift index 0d3c814a..71bbc62c 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/CenterProfileCoordinator.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RegisterCenterInfo/CenterProfileCoordinator.swift @@ -12,36 +12,46 @@ import Entity /// 내센터, 다른 센터를 모두 불러올 수 있습니다. public class CenterProfileCoordinator: ChildCoordinator { + + public struct Dependency { + let mode: ProfileMode + let profileUseCase: CenterProfileUseCase + let navigationController: UINavigationController + + public init(mode: ProfileMode, profileUseCase: CenterProfileUseCase, navigationController: UINavigationController) { + self.mode = mode + self.profileUseCase = profileUseCase + self.navigationController = navigationController + } + } public weak var viewControllerRef: UIViewController? - public weak var parent: CenterProfileRegisterCoordinatable? + public weak var parent: ParentCoordinator? public let navigationController: UINavigationController + let mode: ProfileMode + let profileUseCase: CenterProfileUseCase - public let viewModel: any CenterProfileViewModelable - - public init( - mode: ProfileMode, - profileUseCase: CenterProfileUseCase, - navigationController: UINavigationController - ) { - self.viewModel = CenterProfileViewModel(mode: mode, useCase: profileUseCase) - self.navigationController = navigationController + public init(dependency: Dependency) { + self.mode = dependency.mode + self.profileUseCase = dependency.profileUseCase + self.navigationController = dependency.navigationController } public func start() { - let vc = CenterProfileViewController(coordinator: self) - vc.bind(viewModel: viewModel) + let vc = CenterProfileViewController() + let vm = CenterProfileViewModel( + mode: mode, + coordinator: self, + useCase: profileUseCase + ) + vc.bind(viewModel: vm) self.viewControllerRef = vc navigationController.pushViewController(vc, animated: true) } public func coordinatorDidFinish() { - parent?.removeChildCoordinator(self) - } - - func closeViewController() { popViewController() - coordinatorDidFinish() + parent?.removeChildCoordinator(self) } } 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 0497c0f5..f1ab026f 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -13,40 +13,10 @@ import DSKit import Entity import BaseFeature -public protocol CenterProfileViewModelable where Input: CenterProfileInputable, Output: CenterProfileOutputable { - associatedtype Input - associatedtype Output - var input: Input { get } - var output: Output? { get } - - var profileMode: ProfileMode { get } -} - -public protocol CenterProfileInputable { - var readyToFetch: PublishRelay { get } - var editingButtonPressed: PublishRelay { get } - var editingFinishButtonPressed: PublishRelay { get } - var editingPhoneNumber: BehaviorRelay { get } - var editingInstruction: BehaviorRelay { get } - var selectedImage: PublishRelay { get } -} - -public protocol CenterProfileOutputable: DefaultAlertOutputable { - var centerName: Driver { get } - var centerLocation: Driver { get } - var centerPhoneNumber: Driver { get } - var centerIntroduction: Driver { get } - var displayingImage: Driver { get } - var isEditingMode: Driver { get } - var editingValidation: Driver { get } -} - public class CenterProfileViewController: BaseViewController { var viewModel: (any CenterProfileViewModelable)? - weak var coordinator: CenterProfileCoordinator? - let navigationBar: NavigationBarType1 = { let bar = NavigationBarType1(navigationTitle: "내 센터 정보") return bar @@ -151,9 +121,7 @@ public class CenterProfileViewController: BaseViewController { private let disposeBag = DisposeBag() - public init(coordinator: CenterProfileCoordinator) { - - self.coordinator = coordinator + public init() { super.init(nibName: nil, bundle: nil) @@ -191,6 +159,7 @@ public class CenterProfileViewController: BaseViewController { ]) let locationIcon = UIImageView.locationMark + locationIcon.tintColor = DSColor.gray700.color let centerLocationStack = HStack( [ @@ -322,13 +291,7 @@ public class CenterProfileViewController: BaseViewController { } private func setObservable() { - - navigationBar - .eventPublisher - .subscribe { [weak coordinator] _ in - coordinator?.closeViewController() - } - .disposed(by: disposeBag) + } public func bind(viewModel: any CenterProfileViewModelable) { @@ -374,6 +337,11 @@ public class CenterProfileViewController: BaseViewController { .disposed(by: disposeBag) } + navigationBar + .eventPublisher + .bind(to: input.exitButtonClicked) + .disposed(by: disposeBag) + // output guard let output = viewModel.output else { fatalError() } @@ -455,6 +423,7 @@ public class CenterProfileViewController: BaseViewController { output .alert? .drive { [weak self] vo in + print("!!") self?.showAlert(vo: vo) } .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift index 1f8bcce9..70709db1 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift @@ -89,7 +89,6 @@ public class OnGoingPostVC: BaseViewController { postTableView.rightAnchor.constraint(equalTo: view.rightAnchor), postTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - } private func setObservable() { @@ -138,39 +137,3 @@ extension OnGoingPostVC: UITableViewDataSource, UITableViewDelegate { return cell } } - -class BoardSortigHeaderView: UIView { - - let sortingTypeButton: ImageTextButton = { - let button = ImageTextButton( - iconImage: DSKitAsset.Icons.chevronDown.image, - position: .postfix - ) - button.label.textString = "정렬 기준" - button.label.attrTextColor = DSKitAsset.Colors.gray300.color - return button - }() - - init() { - super.init(frame: .zero) - setLayout() - } - - required init?(coder: NSCoder) { fatalError() } - - func setLayout() { - - [ - sortingTypeButton - ].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - self.addSubview($0) - } - - NSLayoutConstraint.activate([ - sortingTypeButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 24), - sortingTypeButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -24), - sortingTypeButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -12), - ]) - } -} 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 index fc5ce1aa..bd4283e4 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift @@ -241,7 +241,10 @@ public class PostDetailForCenterVC: BaseViewController { } private func setObservable() { - + // 지도뷰 풀스크린 + // 재사용률이 떨어져 ViewController에 직접 삽입합니다. + let fullMapVC = WorkPlaceAndWorkerLocationFullVC() + navigationController?.pushViewController(fullMapVC, animated: true) } public func bind(viewModel: PostDetailViewModelable) { @@ -292,7 +295,7 @@ public class PostDetailForCenterVC: BaseViewController { viewModel .workerEmployCardVO? .drive(onNext: { [sampleCard] vo in - sampleCard.bind(vo: vo) + sampleCard.bind(ro: .create(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 b92f47d1..9b341542 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 @@ -342,7 +342,7 @@ public class PostOverviewVC: BaseViewController { viewModel .workerEmployCardVO? .drive(onNext: { [sampleCard] vo in - sampleCard.bind(vo: vo) + sampleCard.bind(ro: .create(vo: vo)) }) .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift index 992b92b1..625841fb 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -12,15 +12,49 @@ import RxCocoa import PresentationCore import UseCaseInterface -public struct ChangeCenterInformation { +struct ChangeCenterInformation { let phoneNumber: String? let introduction: String? let image: UIImage? } +public protocol CenterProfileViewModelable where Input: CenterProfileInputable, Output: CenterProfileOutputable { + associatedtype Input + associatedtype Output + var input: Input { get } + var output: Output? { get } + + var profileMode: ProfileMode { get } +} + +public protocol CenterProfileInputable { + var readyToFetch: PublishRelay { get } + var editingButtonPressed: PublishRelay { get } + var editingFinishButtonPressed: PublishRelay { get } + var editingPhoneNumber: BehaviorRelay { get } + var editingInstruction: BehaviorRelay { get } + var selectedImage: PublishRelay { get } + + var exitButtonClicked: PublishRelay { get } +} + +public protocol CenterProfileOutputable { + var centerName: Driver { get } + var centerLocation: Driver { get } + var centerPhoneNumber: Driver { get } + var centerIntroduction: Driver { get } + var displayingImage: Driver { get } + var isEditingMode: Driver { get } + var editingValidation: Driver { get } + + var alert: Driver? { get } +} + + public class CenterProfileViewModel: CenterProfileViewModelable { let profileUseCase: CenterProfileUseCase + weak var coordinator: CenterProfileCoordinator? public var input: Input public var output: Output? = nil @@ -45,9 +79,16 @@ public class CenterProfileViewModel: CenterProfileViewModelable { ) } - public init(mode: ProfileMode, useCase: CenterProfileUseCase) { + let disposeBag = DisposeBag() + + public init( + mode: ProfileMode, + coordinator: CenterProfileCoordinator, + useCase: CenterProfileUseCase) + { self.profileMode = mode + self.coordinator = coordinator self.profileUseCase = useCase self.input = Input() @@ -66,7 +107,15 @@ public class CenterProfileViewModel: CenterProfileViewModelable { let profileRequestFailure = profileRequestResult .compactMap { $0.error } .map { error in - DefaultAlertContentVO(title: "프로필 정보 불러오기 실패", message: error.message) + AlertWithCompletionVO( + title: "프로필 정보 불러오기 실패", + message: error.message, + buttonInfo: [ + ("닫기", { [weak self] in + self?.coordinator?.coordinatorDidFinish() + }) + ] + ) } let centerNameDriver = profileRequestSuccess @@ -122,7 +171,7 @@ public class CenterProfileViewModel: CenterProfileViewModelable { let imageValidationFailure = imageValidationResult .filter { $0 == nil } .map { _ in - DefaultAlertContentVO( + AlertWithCompletionVO( title: "이미지 선택 오류", message: "지원하지 않는 이미지 형식입니다." ) @@ -177,7 +226,7 @@ public class CenterProfileViewModel: CenterProfileViewModelable { .compactMap({ $0.error }) .map({ error in // 변경 실패 Alert - return DefaultAlertContentVO( + return AlertWithCompletionVO( title: "변경 실패", message: "변경 싪패 이유" ) @@ -218,6 +267,13 @@ public class CenterProfileViewModel: CenterProfileViewModelable { imageValidationFailure ) .asDriver(onErrorJustReturn: .default) + + // MARK: Exit Button + input.exitButtonClicked + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) self.output = .init( centerName: centerNameDriver, @@ -245,7 +301,6 @@ public class CenterProfileViewModel: CenterProfileViewModelable { public extension CenterProfileViewModel { class Input: CenterProfileInputable { - // ViewController에서 받아오는 데이터 public var readyToFetch: PublishRelay = .init() public var editingButtonPressed: PublishRelay = .init() @@ -253,6 +308,7 @@ public extension CenterProfileViewModel { public var editingPhoneNumber: BehaviorRelay = .init(value: "") public var editingInstruction: BehaviorRelay = .init(value: "") public var selectedImage: PublishRelay = .init() + public var exitButtonClicked: RxRelay.PublishRelay = .init() } class Output: CenterProfileOutputable { @@ -269,9 +325,9 @@ 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) { + init(centerName: Driver, centerLocation: Driver, centerPhoneNumber: Driver, centerIntroduction: Driver, displayingImage: Driver, isEditingMode: Driver, editingValidation: Driver, alert: Driver) { self.centerName = centerName self.centerLocation = centerLocation self.centerPhoneNumber = centerPhoneNumber diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift rename to project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift index ac2dd707..cb488146 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift @@ -121,65 +121,12 @@ public class PostDetailForCenterVM: PostDetailViewModelable { 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 + return WorkerEmployCardVO.create( + workTimeAndPay: fetched_workTimeAndPay.value, + customerRequirement: fetched_customerRequirement.value, + customerInformation: fetched_customerInformation.value, + applicationDetail: fetched_applicationDetail.value, + addressInfo: fetched_addressInfo.value ) } .asDriver(onErrorJustReturn: .default) 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 03d4f992..b64439ec 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift @@ -389,69 +389,17 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { .create { [ editing_workTimeAndPay, editing_customerInformation, + editing_customerRequirement, editing_applicationDetail, editing_addressInfo ] emitter in - // 남은 일수 - var leftDay: Int? = nil - let calendar = Calendar.current - let currentDate = Date() - - if editing_applicationDetail.value.applyDeadlineType == .specificDate, let deadlineDate = editing_applicationDetail.value.deadlineDate { - - let component = calendar.dateComponents([.day], from: currentDate, to: deadlineDate) - leftDay = component.day - } - - // 초보가능 여부 - let isBeginnerPossible = editing_applicationDetail.value.experiencePreferenceType == .beginnerPossible - - // 제목(=도로명주소) - let title = editing_addressInfo.value.addressInfo?.roadAddress ?? "위치정보 표기 오류" - - // 도보시간 - let timeTakenForWalk = "도보 n분" - - // 생년 - let birthYear = Int(editing_customerInformation.value.birthYear) ?? 1970 - let currentYear = calendar.component(.year, from: currentDate) - let targetAge = currentYear - birthYear + 1 - - // 요양등급 - let targetLavel: Int = (editing_customerInformation.value.careGrade?.rawValue ?? 0)+1 - - // 성별 - let targetGender = editing_customerInformation.value.gender - - // 근무 요일 - let days = editing_workTimeAndPay.value.selectedDays.filter { (_, value) in - value - }.map { (key, _) in - key - } - - // 근무 시작, 종료시간 - let startTime = editing_workTimeAndPay.value.workStartTime?.convertToStringForButton() ?? "00:00" - let workEndTime = editing_workTimeAndPay.value.workEndTime?.convertToStringForButton() ?? "00:00" - - // 급여타입및 양 - let paymentType = editing_workTimeAndPay.value.paymentType ?? .hourly - let paymentAmount = editing_workTimeAndPay.value.paymentAmount - - let vo = 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 + let vo = WorkerEmployCardVO.create( + workTimeAndPay: editing_workTimeAndPay.value, + customerRequirement: editing_customerRequirement.value, + customerInformation: editing_customerInformation.value, + applicationDetail: editing_applicationDetail.value, + addressInfo: editing_addressInfo.value ) emitter.onNext(vo) diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/AppliedAndLiked/AppliedAndLikedBoardCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/AppliedAndLiked/AppliedAndLikedBoardCoordinator.swift new file mode 100644 index 00000000..5f6c552f --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/AppliedAndLiked/AppliedAndLikedBoardCoordinator.swift @@ -0,0 +1,93 @@ +// +// AppliedAndLikedBoardCoordinator.swift +// RootFeature +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import WorkerFeature +import BaseFeature +import CenterFeature +import PresentationCore +import UseCaseInterface + +public class AppliedAndLikedBoardCoordinator: WorkerRecruitmentBoardCoordinatable { + + public struct Dependency { + let navigationController: UINavigationController + let centerProfileUseCase: CenterProfileUseCase + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(navigationController: UINavigationController, centerProfileUseCase: CenterProfileUseCase, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.navigationController = navigationController + self.centerProfileUseCase = centerProfileUseCase + self.recruitmentPostUseCase = recruitmentPostUseCase + } + } + + public var childCoordinators: [any PresentationCore.Coordinator] = [] + + public weak var viewControllerRef: UIViewController? + + public var navigationController: UINavigationController + + weak var parent: ParentCoordinator? + + let centerProfileUseCase: CenterProfileUseCase + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(depedency: Dependency) { + self.navigationController = depedency.navigationController + self.centerProfileUseCase = depedency.centerProfileUseCase + self.recruitmentPostUseCase = depedency.recruitmentPostUseCase + } + + public func start() { + let vc = StarredAndAppliedVC() + let appliedVM = AppliedPostBoardVM( + recruitmentPostUseCase: recruitmentPostUseCase + ) + let starredVM = StarredPostBoardVM( + recruitmentPostUseCase: recruitmentPostUseCase + ) + vc.bind( + appliedPostVM: appliedVM, + starredPostVM: starredVM + ) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: false) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } +} + +extension AppliedAndLikedBoardCoordinator { + public func showPostDetail(postId: String) { + let coodinator = PostDetailForWorkerCoodinator( + dependency: .init( + postId: postId, + parent: self, + navigationController: navigationController, + recruitmentPostUseCase: recruitmentPostUseCase + ) + ) + addChildCoordinator(coodinator) + coodinator.start() + } + public func showCenterProfile(centerId: String) { + let coordinator = CenterProfileCoordinator( + dependency: .init( + mode: .otherProfile(id: centerId), + profileUseCase: centerProfileUseCase, + navigationController: navigationController + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } +} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/ApplyManagementCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/ApplyManagementCoordinator.swift deleted file mode 100644 index de8d7645..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/ApplyManagementCoordinator.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ApplyManagementCoordinator.swift -// RootFeature -// -// Created by choijunios on 7/25/24. -// - -import UIKit -import PresentationCore - -public class ApplyManagementCoordinator: ChildCoordinator { - - public weak var viewControllerRef: UIViewController? - - public var navigationController: UINavigationController - - public init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - public func start() { - let vc = ApplyManagementVC() - - navigationController.pushViewController(vc, animated: false) - } - - public func coordinatorDidFinish() { - - } -} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/MainBoard/WorkerRecruitmentBoardCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/MainBoard/WorkerRecruitmentBoardCoordinator.swift new file mode 100644 index 00000000..623d9d61 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/MainBoard/WorkerRecruitmentBoardCoordinator.swift @@ -0,0 +1,89 @@ +// +// WorkerRecruitmentBoardCoordinator.swift +// RootFeature +// +// Created by choijunios on 7/25/24. +// + +import UIKit +import WorkerFeature +import BaseFeature +import CenterFeature +import PresentationCore +import UseCaseInterface + +public class WorkerRecruitmentBoardCoordinator: WorkerRecruitmentBoardCoordinatable { + + public struct Dependency { + let navigationController: UINavigationController + let centerProfileUseCase: CenterProfileUseCase + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(navigationController: UINavigationController, centerProfileUseCase: CenterProfileUseCase, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.navigationController = navigationController + self.centerProfileUseCase = centerProfileUseCase + self.recruitmentPostUseCase = recruitmentPostUseCase + } + } + + public var childCoordinators: [any PresentationCore.Coordinator] = [] + + public weak var viewControllerRef: UIViewController? + + public var navigationController: UINavigationController + + weak var parent: ParentCoordinator? + + let centerProfileUseCase: CenterProfileUseCase + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(depedency: Dependency) { + self.navigationController = depedency.navigationController + self.centerProfileUseCase = depedency.centerProfileUseCase + self.recruitmentPostUseCase = depedency.recruitmentPostUseCase + } + + public func start() { + let vc = WorkerRecruitmentPostBoardVC() + let vm = WorkerRecruitmentPostBoardVM( + coordinator: self, + recruitmentPostUseCase: recruitmentPostUseCase + ) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: false) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } +} + +extension WorkerRecruitmentBoardCoordinator { + public func showPostDetail(postId: String) { + let coodinator = PostDetailForWorkerCoodinator( + dependency: .init( + postId: postId, + parent: self, + navigationController: navigationController, + recruitmentPostUseCase: recruitmentPostUseCase + ) + ) + addChildCoordinator(coodinator) + coodinator.start() + } + public func showCenterProfile(centerId: String) { + let coordinator = CenterProfileCoordinator( + dependency: .init( + mode: .otherProfile(id: centerId), + profileUseCase: centerProfileUseCase, + navigationController: navigationController + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } +} + diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/RecruitmentBoardCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/RecruitmentBoardCoordinator.swift deleted file mode 100644 index b16bee40..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/RecruitmentBoardCoordinator.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// RecruitmentBoardCoordinator.swift -// RootFeature -// -// Created by choijunios on 7/25/24. -// - -import UIKit -import PresentationCore - -public class RecruitmentBoardCoordinator: ChildCoordinator { - - public weak var viewControllerRef: UIViewController? - - public var navigationController: UINavigationController - - public init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - public func start() { - let vc = RecruitmentBoardVC() - - navigationController.pushViewController(vc, animated: false) - } - - public func coordinatorDidFinish() { - - } -} - diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/SettingCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/Setting/SettingCoordinator.swift similarity index 100% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/SettingCoordinator.swift rename to project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/Setting/SettingCoordinator.swift diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/ RecruitmentBoardVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ RecruitmentBoardVC.swift similarity index 96% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/ RecruitmentBoardVC.swift rename to project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ RecruitmentBoardVC.swift index 7444898a..be5270f5 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/ RecruitmentBoardVC.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ RecruitmentBoardVC.swift @@ -1,5 +1,5 @@ // -// RecruitmentBoardVC.swift +// RecruitmentBoardVC.swift // RootFeature // // Created by choijunios on 7/25/24. diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/ApplyManagementVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ApplyManagementVC.swift similarity index 100% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/ApplyManagementVC.swift rename to project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ApplyManagementVC.swift diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift new file mode 100644 index 00000000..70c620a3 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift @@ -0,0 +1,188 @@ +// +// StarredAndAppliedVC.swift +// WorkerFeature +// +// Created by choijunios on 8/16/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import CenterFeature + +public protocol WorkerStaticPostBoardVMable { + + var postBoardData: Driver<[WorkerEmployCardViewModelable]>? { get } + var postViewWillAppear: PublishRelay { get } + + var alert: Driver? { get } +} + +public class StarredAndAppliedVC: BaseViewController { + enum TabBarState: Int, CaseIterable { + case applied = 0 + case starred = 1 + + var titleText: String { + switch self { + case .applied: + "지원한 공고" + case .starred: + "찜한 공고" + } + } + } + struct TabBarItem: IdleTabItem { + var id: TabBarState + var tabLabelText: String + + init(id: TabBarState) { + self.id = id + self.tabLabelText = id.titleText + } + } + + private var currentState: TabBarState = .applied + private let viewControllerDict: [TabBarState: WorkerStaticPostBoardVC] = [ + .applied : WorkerStaticPostBoardVC(), + .starred : WorkerStaticPostBoardVC() + ] + + // Init + + // View + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + label.textString = "공고 관리" + return label + }() + + lazy var tabBar: IdleTabControlBar = .init( + items: TabBarState.allCases.map { TabBarItem(id: $0) }, + initialItem: .init(id: currentState) + )! + + // 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() + + addViewControllerAndSetLayout(vc: viewControllerDict[currentState]!) + } + + private func setAppearance() { + view.backgroundColor = DSKitAsset.Colors.gray0.color + } + + private func setLayout() { + [ + titleLabel, + tabBar, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 21), + titleLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + + tabBar.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + tabBar.leftAnchor.constraint(equalTo: view.leftAnchor), + tabBar.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + } + + private func setObservable() { + tabBar + .statePublisher + .subscribe(onNext: { [weak self] item in + self?.showViewController(state: item.id) + }) + .disposed(by: disposeBag) + } + + private func showViewController(state: TabBarState) { + + if currentState == state { return } + + // 탭바터치 정지 + tabBar.isUserInteractionEnabled = false + + /// viewWillAppear이후에 호출 + let prevViewController = viewControllerDict[currentState] + let vc = viewControllerDict[state]! + + let prevIndex = currentState.rawValue + let currentIndex = state.rawValue + + addViewControllerAndSetLayout(vc: vc) + + vc.view.transform = .init(translationX: view.bounds.width * (prevIndex < currentIndex ? 1 : -1), y: 0) + + UIView.animate(withDuration: 0.2) { [weak self] in + + guard let self else { return } + + vc.view.transform = .identity + prevViewController?.view.transform = .init(translationX: (prevIndex < currentIndex ? -1 : 1) * view.bounds.width, y: 0) + + } completion: { [weak self] _ in + + prevViewController?.view.removeFromSuperview() + + prevViewController?.willMove(toParent: nil) + prevViewController?.removeFromParent() + + self?.currentState = state + self?.tabBar.isUserInteractionEnabled = true + } + } + + private func addViewControllerAndSetLayout(vc: UIViewController) { + addChild(vc) + view.addSubview(vc.view) + vc.didMove(toParent: self) + vc.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + vc.view.topAnchor.constraint(equalTo: tabBar.bottomAnchor), + vc.view.leftAnchor.constraint(equalTo: view.leftAnchor), + vc.view.rightAnchor.constraint(equalTo: view.rightAnchor), + vc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + public func bind( + appliedPostVM: WorkerStaticPostBoardVMable, + starredPostVM: WorkerStaticPostBoardVMable + ) { + + viewControllerDict[.applied]?.bind(viewModel: appliedPostVM) + viewControllerDict[.starred]?.bind(viewModel: starredPostVM) + + Observable + .merge( + appliedPostVM.alert?.asObservable() ?? .empty(), + starredPostVM.alert?.asObservable() ?? .empty() + ) + .subscribe(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + } +} + diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/SubVC/WorkerStaticPostBoardVC.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/SubVC/WorkerStaticPostBoardVC.swift new file mode 100644 index 00000000..6fe0db72 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/SubVC/WorkerStaticPostBoardVC.swift @@ -0,0 +1,128 @@ +// +// WorkerStaticPostBoardVC.swift +// WorkerFeature +// +// Created by choijunios on 8/16/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class WorkerStaticPostBoardVC: BaseViewController { + + typealias Cell = WorkerEmployCardCell + + var viewModel: WorkerStaticPostBoardVMable? + + // View + let postTableView: UITableView = { + let tableView = UITableView() + tableView.rowHeight = UITableView.automaticDimension + tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) + return tableView + }() + + let tableHeader = BoardSortigHeaderView() + + let postViewModels: BehaviorRelay<[WorkerEmployCardViewModelable]> = .init(value: []) + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + setAppearance() + setLayout() + setObservable() + setTableView() + } + + private func setTableView() { + postTableView.dataSource = self + postTableView.delegate = self + postTableView.separatorStyle = .none + postTableView.delaysContentTouches = false + + postTableView.tableHeaderView = tableHeader + + tableHeader.frame = .init(origin: .zero, size: .init( + width: view.bounds.width, + height: 60) + ) + } + + private func setAppearance() { + + } + + private func setLayout() { + + [ + postTableView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + postTableView.topAnchor.constraint(equalTo: view.topAnchor), + postTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + postTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + postTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + private func setObservable() { + + } + + func bind(viewModel: WorkerStaticPostBoardVMable) { + + self.viewModel = viewModel + + // Output + viewModel + .postBoardData? + .drive(onNext: { [weak self] viewModels in + guard let self else { return } + self.postViewModels.accept(viewModels) + self.postTableView.reloadData() + }) + .disposed(by: disposeBag) + + // Input + rx.viewWillAppear + .map { _ in } + .bind(to: viewModel.postViewWillAppear) + .disposed(by: disposeBag) + } +} + +extension WorkerStaticPostBoardVC: UITableViewDataSource, UITableViewDelegate { + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + postViewModels.value.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell + cell.selectionStyle = .none + + let vm = postViewModels.value[indexPath.row] + cell.bind(viewModel: vm) + + return cell + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVC.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVC.swift new file mode 100644 index 00000000..1189da2d --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVC.swift @@ -0,0 +1,253 @@ +// +// WorkerRecruitmentPostBoardVC.swift +// WorkerFeature +// +// Created by choijunios on 8/15/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + + +public class WorkerRecruitmentPostBoardVC: BaseViewController { + typealias Cell = WorkerEmployCardCell + + var viewModel: WorkerRecruitmentPostBoardVMable? + + // View + fileprivate let topContainer: WorkerMainTopContainer = { + let container = WorkerMainTopContainer(innerViews: []) + return container + }() + let postTableView: UITableView = { + let tableView = UITableView() + tableView.rowHeight = UITableView.automaticDimension + tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) + return tableView + }() + let tableHeader = BoardSortigHeaderView() + + // Paging + var isPaging = true + + // Observable + let postViewModels: BehaviorRelay<[WorkerEmployCardViewModelable]> = .init(value: []) + let requestNextPage: PublishRelay = .init() + + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + setTableView() + setLayout() + } + + public func bind(viewModel: WorkerRecruitmentPostBoardVMable) { + + self.viewModel = viewModel + + // Output + viewModel + .workerLocationTitleText? + .drive(onNext: { [weak self] titleText in + self?.topContainer.locationLabel.textString = titleText + }) + .disposed(by: disposeBag) + + viewModel + .postBoardData? + .drive(onNext: { [weak self] viewModels in + guard let self else { return } + self.postViewModels.accept(viewModels) + self.postTableView.reloadData() + self.isPaging = false + }) + .disposed(by: disposeBag) + + viewModel + .alert? + .drive(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + + // Input + self.rx.viewDidLoad + .bind(to: viewModel.viewDidLoad) + .disposed(by: disposeBag) + + self.requestNextPage + .bind(to: viewModel.requestNextPage) + .disposed(by: disposeBag) + } + + private func setTableView() { + postTableView.dataSource = self + postTableView.delegate = self + postTableView.separatorStyle = .none + postTableView.delaysContentTouches = false + + postTableView.tableHeaderView = tableHeader + + tableHeader.frame = .init(origin: .zero, size: .init( + width: view.bounds.width, + height: 60) + ) + } + + private func setLayout() { + + [ + topContainer, + postTableView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + topContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + topContainer.leftAnchor.constraint(equalTo: view.leftAnchor), + topContainer.rightAnchor.constraint(equalTo: view.rightAnchor), + + postTableView.topAnchor.constraint(equalTo: topContainer.bottomAnchor), + postTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + postTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + postTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + private func setObservable() { + + } +} + +extension WorkerRecruitmentPostBoardVC: UITableViewDataSource, UITableViewDelegate { + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + postViewModels.value.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell + cell.selectionStyle = .none + + let vm = postViewModels.value[indexPath.row] + cell.bind(viewModel: vm) + + return cell + } +} + +// MARK: ScrollView관련 +extension WorkerRecruitmentPostBoardVC { + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + let offsetY = scrollView.contentOffset.y + let contentHeight = scrollView.contentSize.height + let height = scrollView.frame.height + + // 스크롤이 테이블 뷰 Offset의 끝에 가게 되면 다음 페이지를 호출 + if offsetY > (contentHeight - height) { + if !isPaging { + isPaging = true + requestNextPage.accept(()) + } + } + } +} + +// MARK: Top Container +fileprivate class WorkerMainTopContainer: UIView { + + // Init parameters + + // View + + lazy var locationLabel: IdleLabel = { + + let label = IdleLabel(typography: .Heading1) + label.textAlignment = .left + return label + }() + + let locationImage: UIImageView = { + let imageView = UIImageView() + imageView.image = DSIcon.location.image + imageView.tintColor = DSColor.gray700.color + return imageView + }() + + private let disposeBag = DisposeBag() + + init( + titleText: String = "", + innerViews: [UIView] + ) { + super.init(frame: .zero) + + self.locationLabel.textString = titleText + + setApearance() + setAutoLayout(innerViews: innerViews) + } + + public required init(coder: NSCoder) { fatalError() } + + func setApearance() { + + } + + private func setAutoLayout(innerViews: [UIView]) { + + self.layoutMargins = .init( + top: 20.43, + left: 20, + bottom: 8, + right: 20 + ) + + let mainStack = HStack( + [ + [ + locationImage, + Spacer(width: 4), + locationLabel, + Spacer(), + ], + innerViews + ].flatMap { $0 }, + alignment: .center, + distribution: .fill + ) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + locationImage.widthAnchor.constraint(equalToConstant: 32), + locationImage.heightAnchor.constraint(equalTo: locationImage.widthAnchor), + + mainStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift new file mode 100644 index 00000000..0cd453e6 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift @@ -0,0 +1,146 @@ +// +// AppliedPostBoardVM.swift +// WorkerFeature +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import UseCaseInterface + + +public class AppliedPostBoardVM: WorkerStaticPostBoardVMable { + + public var postViewWillAppear: RxRelay.PublishRelay = .init() + + public var postBoardData: RxCocoa.Driver<[any DSKit.WorkerEmployCardViewModelable]>? + public var alert: RxCocoa.Driver? + + // Init + weak var coordinator: WorkerRecruitmentBoardCoordinatable? + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(recruitmentPostUseCase: RecruitmentPostUseCase) { + self.recruitmentPostUseCase = recruitmentPostUseCase + + let requestPostResult = postViewWillAppear + .flatMap { [unowned self] _ in + self.publishAppliedPostMocks() + } + .share() + + let requestPostSuccess = requestPostResult.compactMap { $0.value } + let requestPostFailure = requestPostResult.compactMap { $0.error } + + postBoardData = requestPostSuccess + .map { postForWorkerVos in + + // ViewModel 생성 + let viewModels = postForWorkerVos.map { vo in + + let cardVO: WorkerEmployCardVO = .create(vo: vo) + + let vm: AppliedWorkerEmployCardVM = .init( + postId: vo.postId, + vo: cardVO, + coordinator: self.coordinator + ) + + return vm + } + + return viewModels + } + .asDriver(onErrorJustReturn: []) + + alert = requestPostFailure + .map { error in + DefaultAlertContentVO( + title: "지원한 공고 불러오기 오류", + message: error.message + ) + } + .asDriver(onErrorJustReturn: .default) + } + + + func publishAppliedPostMocks() -> Single> { + return .just(.success((0..<10).map { _ in .mock })) + } +} + +class AppliedWorkerEmployCardVM: WorkerEmployCardViewModelable { + + weak var coordinator: WorkerRecruitmentBoardCoordinatable? + + // Init + let postId: String + + public var renderObject: RxCocoa.Driver? + public var applicationInformation: RxCocoa.Driver? + + public var cardClicked: RxRelay.PublishRelay = .init() + public var applyButtonClicked: RxRelay.PublishRelay = .init() + public var starButtonClicked: RxRelay.PublishRelay = .init() + + let disposeBag = DisposeBag() + + public init + ( + postId: String, + vo: WorkerEmployCardVO, + coordinator: WorkerRecruitmentBoardCoordinatable? = nil + ) + { + self.postId = postId + self.coordinator = coordinator + + // MARK: 지원여부 + let applicationInformation: BehaviorRelay = .init( + value: .init( + isApplied: true, + applicationDateText: "날자정보 (미구현)" + ) + ) + self.applicationInformation = applicationInformation.asDriver() + + // MARK: Card RenderObject + let workerEmployCardRO: BehaviorRelay = .init(value: .mock) + renderObject = workerEmployCardRO.asDriver(onErrorJustReturn: .mock) + + workerEmployCardRO.accept(WorkerEmployCardRO.create(vo: vo)) + + // MARK: 버튼 처리 + applyButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + // 지원하기 버튼 눌림 + }) + .disposed(by: disposeBag) + + cardClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + coordinator?.showPostDetail( + postId: postId + ) + }) + .disposed(by: disposeBag) + + starButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + // 즐겨찾기 버튼눌림 + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVM.swift new file mode 100644 index 00000000..9f8a5dab --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVM.swift @@ -0,0 +1,239 @@ +// +// WorkerRecruitmentPostBoardVM.swift +// WorkerFeature +// +// Created by choijunios on 8/15/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import UseCaseInterface + +public protocol WorkerRecruitmentPostBoardVMable: DefaultAlertOutputable { + + /// 다음 페이지를 요청합니다. + var requestNextPage: PublishRelay { get } + /// ViewDidLoad + var viewDidLoad: PublishRelay { get } + + + /// 페이지요청에 대한 결과를 전달합니다. + var postBoardData: Driver<[WorkerEmployCardViewModelable]>? { get } + /// 요양보호사 위치 정보를 전달합니다. + var workerLocationTitleText: Driver? { get } +} + +public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { + + // Output + public var postBoardData: Driver<[WorkerEmployCardViewModelable]>? + public var alert: Driver? + public var workerLocationTitleText: Driver? + + + + // Input + public var viewDidLoad: PublishRelay = .init() + public var requestNextPage: PublishRelay = .init() + + + + // Init + weak var coordinator: WorkerRecruitmentBoardCoordinatable? + let recruitmentPostUseCase: RecruitmentPostUseCase + + // Paging + /// 값이 nil이라면 요청을 보내지 않습니다. + var nextPagingRequest: PostPagingRequestForWorker? + /// 가장최신의 데이터를 홀드, 다음 요청시 해당데이터에 새로운 데이터를 더해서 방출 + private let currentPostVO: BehaviorRelay<[RecruitmentPostForWorkerVO]> = .init(value: []) + + // Observable + let dispostBag = DisposeBag() + + public init( + coordinator: WorkerRecruitmentBoardCoordinatable, + recruitmentPostUseCase: RecruitmentPostUseCase + ) + { + self.coordinator = coordinator + self.recruitmentPostUseCase = recruitmentPostUseCase + self.nextPagingRequest = .native(nextPageId: nil) + + // 상단 위치정보 + workerLocationTitleText = viewDidLoad + .compactMap { [weak self] _ in + self?.fetchWorkerLocation() + } + .asDriver(onErrorJustReturn: "반갑습니다.") + + + let postPageReqeustResult = Observable + .merge( + viewDidLoad.asObservable(), + requestNextPage.asObservable() + ) + .compactMap { [weak self] _ in + // 요청이 없는 경우 요청을 보내지 않는다. + // ThirdPatry에서도 불러올 데이터가 없는 경우입니다. + self?.nextPagingRequest + } + .share() + .flatMap { [recruitmentPostUseCase] request in + recruitmentPostUseCase + .getPostListForWorker( + request: request, + postCount: 10 + ) + } + .share() + + + let requestPostListSuccess = postPageReqeustResult.compactMap { $0.value } + let requestPostListFailure = postPageReqeustResult.compactMap { $0.error } + + postBoardData = Observable + .zip( + currentPostVO, + requestPostListSuccess + ) + .compactMap { [weak self] (prevPostList, fetchedData) -> [WorkerEmployCardViewModelable]? in + + guard let self else { return nil } + + // 다음 요청설정 + var nextRequest: PostPagingRequestForWorker? + if let prevRequest = self.nextPagingRequest { + + if let nextPageId = fetchedData.nextPageId { + // 다음값이 있는 경우 + switch prevRequest { + case .native: + nextRequest = .native(nextPageId: nextPageId) + case .thirdParty: + nextRequest = .thirdParty(nextPageId: nextPageId) + } + } else { + // 다음값이 없는 경우 + switch prevRequest { + case .native: + nextRequest = .thirdParty(nextPageId: nil) + case .thirdParty: + // 페이징 종료 + nextRequest = nil + } + } + } + self.nextPagingRequest = nextRequest + + // 화면에 표시할 전체리스트 도출 + let fetchedPosts = fetchedData.posts + var mergedPosts = currentPostVO.value + mergedPosts.append(contentsOf: fetchedPosts) + + // 최근값 업데이트 + self.currentPostVO.accept(mergedPosts) + + // ViewModel 생성 + let viewModels = mergedPosts.map { vo in + + let cardVO: WorkerEmployCardVO = .create(vo: vo) + + let vm: OngoindWorkerEmployCardVM = .init( + postId: vo.postId, + vo: cardVO, + coordinator: self.coordinator + ) + + return vm + } + + return viewModels + } + .asDriver(onErrorJustReturn: []) + + alert = requestPostListFailure + .map { error in + return DefaultAlertContentVO( + title: "시스템 오류", + message: error.message + ) + } + .asDriver(onErrorJustReturn: .default) + } + + /// Test + func fetchWorkerLocation() -> String { + "서울시 영등포구" + } +} + +class OngoindWorkerEmployCardVM: WorkerEmployCardViewModelable { + + weak var coordinator: WorkerRecruitmentBoardCoordinatable? + + // Init + let postId: String + + public var renderObject: RxCocoa.Driver? + public var applicationInformation: RxCocoa.Driver? + + public var cardClicked: RxRelay.PublishRelay = .init() + public var applyButtonClicked: RxRelay.PublishRelay = .init() + public var starButtonClicked: RxRelay.PublishRelay = .init() + + let disposeBag = DisposeBag() + + public init + ( + postId: String, + vo: WorkerEmployCardVO, + coordinator: WorkerRecruitmentBoardCoordinatable? = nil + ) + { + self.postId = postId + self.coordinator = coordinator + + // MARK: 지원여부 + let applicationInformation: BehaviorRelay = .init(value: .mock) + self.applicationInformation = applicationInformation.asDriver() + + // MARK: Card RenderObject + let workerEmployCardRO: BehaviorRelay = .init(value: .mock) + renderObject = workerEmployCardRO.asDriver(onErrorJustReturn: .mock) + + workerEmployCardRO.accept(WorkerEmployCardRO.create(vo: vo)) + + // MARK: 버튼 처리 + applyButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + // 지원하기 버튼 눌림 + }) + .disposed(by: disposeBag) + + cardClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + coordinator?.showPostDetail( + postId: postId + ) + }) + .disposed(by: disposeBag) + + starButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + // 즐겨찾기 버튼눌림 + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift new file mode 100644 index 00000000..4070ff96 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift @@ -0,0 +1,145 @@ +// +// StarredPostBoardVM.swift +// WorkerFeature +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import UseCaseInterface + +public class StarredPostBoardVM: WorkerStaticPostBoardVMable { + + public var postViewWillAppear: RxRelay.PublishRelay = .init() + + public var postBoardData: RxCocoa.Driver<[any DSKit.WorkerEmployCardViewModelable]>? + public var alert: RxCocoa.Driver? + + // Init + weak var coordinator: WorkerRecruitmentBoardCoordinatable? + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(recruitmentPostUseCase: RecruitmentPostUseCase) { + self.recruitmentPostUseCase = recruitmentPostUseCase + + let requestPostResult = postViewWillAppear + .flatMap { [unowned self] _ in + self.publishStarredPostMocks() + } + .share() + + let requestPostSuccess = requestPostResult.compactMap { $0.value } + let requestPostFailure = requestPostResult.compactMap { $0.error } + + postBoardData = requestPostSuccess + .map { postForWorkerVos in + + // ViewModel 생성 + let viewModels = postForWorkerVos.map { vo in + + let cardVO: WorkerEmployCardVO = .create(vo: vo) + + let vm: StarredWorkerEmployCardVM = .init( + postId: vo.postId, + vo: cardVO, + coordinator: self.coordinator + ) + + return vm + } + + return viewModels + } + .asDriver(onErrorJustReturn: []) + + alert = requestPostFailure + .map { error in + DefaultAlertContentVO( + title: "즐겨찾기한 공고 불러오기 오류", + message: error.message + ) + } + .asDriver(onErrorJustReturn: .default) + } + + + func publishStarredPostMocks() -> Single> { + return .just(.success((0..<10).map { _ in .mock })) + } +} + +class StarredWorkerEmployCardVM: WorkerEmployCardViewModelable { + + weak var coordinator: WorkerRecruitmentBoardCoordinatable? + + // Init + let postId: String + + public var renderObject: RxCocoa.Driver? + public var applicationInformation: RxCocoa.Driver? + + public var cardClicked: RxRelay.PublishRelay = .init() + public var applyButtonClicked: RxRelay.PublishRelay = .init() + public var starButtonClicked: RxRelay.PublishRelay = .init() + + let disposeBag = DisposeBag() + + public init + ( + postId: String, + vo: WorkerEmployCardVO, + coordinator: WorkerRecruitmentBoardCoordinatable? = nil + ) + { + self.postId = postId + self.coordinator = coordinator + + // MARK: 지원여부 + let applicationInformation: BehaviorRelay = .init( + value: .init( + isApplied: false, + applicationDateText: "" + ) + ) + self.applicationInformation = applicationInformation.asDriver() + + // MARK: Card RenderObject + let workerEmployCardRO: BehaviorRelay = .init(value: .mock) + renderObject = workerEmployCardRO.asDriver(onErrorJustReturn: .mock) + + workerEmployCardRO.accept(WorkerEmployCardRO.create(vo: vo)) + + // MARK: 버튼 처리 + applyButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + // 지원하기 버튼 눌림 + }) + .disposed(by: disposeBag) + + cardClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + coordinator?.showPostDetail( + postId: postId + ) + }) + .disposed(by: disposeBag) + + starButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + // 즐겨찾기 버튼눌림 + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CenterPostBoardCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/CenterPostBoardCoordinatable.swift similarity index 100% rename from project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CenterPostBoardCoordinatable.swift rename to project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/CenterPostBoardCoordinatable.swift diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CheckApplicantCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/CheckApplicantCoordinatable.swift similarity index 100% rename from project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CheckApplicantCoordinatable.swift rename to project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/CheckApplicantCoordinatable.swift diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RecruitmentManagementCoordinatable.swift similarity index 100% rename from project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift rename to project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RecruitmentManagementCoordinatable.swift diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RegisterRecruitmentPostCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RegisterRecruitmentPostCoordinatable.swift similarity index 100% rename from project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RegisterRecruitmentPostCoordinatable.swift rename to project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RegisterRecruitmentPostCoordinatable.swift diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Worker/WorkerRecruitmentBoardCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Worker/WorkerRecruitmentBoardCoordinatable.swift new file mode 100644 index 00000000..721df7cd --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Worker/WorkerRecruitmentBoardCoordinatable.swift @@ -0,0 +1,16 @@ +// +// WorkerRecruitmentBoardCoordinatable.swift +// PresentationCore +// +// Created by choijunios on 8/15/24. +// + +import Foundation + +public protocol WorkerRecruitmentBoardCoordinatable: ParentCoordinator { + /// 요양보호사가 볼 수 있는 공고 상세정보를 표시합니다. + func showPostDetail(postId: String) + + /// 센터 프로필을 표시합니다. + func showCenterProfile(centerId: String) +}