diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift index 92457d08..d52bc0ad 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift @@ -43,9 +43,11 @@ class AppliedAndLikedBoardCoordinator: WorkerRecruitmentBoardCoordinatable { public func start() { let vc = StarredAndAppliedVC() let appliedVM = AppliedPostBoardVM( + coordinator: self, recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) let starredVM = StarredPostBoardVM( + coordinator: self, recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) vc.bind( diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift index 8d68e1c0..fc171c1b 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift @@ -60,4 +60,17 @@ class WorkerSettingCoordinaator: WorkerSettingScreenCoordinatable { coordinator.parent = self coordinator.start() } + + public func showMyProfileScreen() { + let coordinator = WorkerProfileCoordinator( + dependency: .init( + profileMode: .myProfile, + navigationController: navigationController, + workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self) + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } } diff --git a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift index 20e3ad76..ba5f0104 100644 --- a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift +++ b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift @@ -14,11 +14,13 @@ import Moya public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { - private var service: RecruitmentPostService = .init() + private var recruitmentPostService: RecruitmentPostService = .init() + private var applyService: ApplyService = .init() public init(_ store: KeyValueStore? = nil) { if let store { - self.service = RecruitmentPostService(keyValueStore: store) + self.recruitmentPostService = RecruitmentPostService(keyValueStore: store) + self.applyService = ApplyService(keyValueStore: store) } } @@ -27,13 +29,13 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { let encodedData = try! JSONEncoder().encode(bundle.toDTO()) - return service.request(api: .registerPost(postData: encodedData), with: .withToken) + return recruitmentPostService.request(api: .registerPost(postData: encodedData), with: .withToken) .mapToVoid() } public func getPostDetailForCenter(id: String) -> RxSwift.Single { - service.request(api: .postDetail(id: id, userType: .center), with: .withToken) + recruitmentPostService.request(api: .postDetail(id: id, userType: .center), with: .withToken) .map(RecruitmentPostFetchDTO.self) .map { dto in dto.toEntity() @@ -44,26 +46,26 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { let encodedData = try! JSONEncoder().encode(bundle.toDTO()) - return service.request( + return recruitmentPostService.request( api: .editPost(id: id, postData: encodedData), with: .withToken ).map { _ in () } } public func getOngoingPosts() -> RxSwift.Single<[Entity.RecruitmentPostInfoForCenterVO]> { - return service.request(api: .getOnGoingPosts, with: .withToken) + return recruitmentPostService.request(api: .getOnGoingPosts, with: .withToken) .map(RecruitmentPostForCenterListDTO.self) .map({ $0.jobPostings.map { $0.toVO() } }) } public func getClosedPosts() -> RxSwift.Single<[Entity.RecruitmentPostInfoForCenterVO]> { - return service.request(api: .getClosedPosts, with: .withToken) + return recruitmentPostService.request(api: .getClosedPosts, with: .withToken) .map(RecruitmentPostForCenterListDTO.self) .map({ $0.jobPostings.map { $0.toVO() } }) } public func getPostApplicantCount(id: String) -> RxSwift.Single { - service.request(api: .getPostApplicantCount(id: id), with: .withToken) + recruitmentPostService.request(api: .getPostApplicantCount(id: id), with: .withToken) .map(PostApplicantCountDTO.self) .map { dto in dto.applicantCount @@ -71,7 +73,7 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { } public func getPostApplicantScreenData(id: String) -> RxSwift.Single { - service.request(api: .getApplicantList(id: id), with: .withToken) + recruitmentPostService.request(api: .getApplicantList(id: id), with: .withToken) .map(PostApplicantScreenDTO.self) .map { dto in dto.toVO() @@ -79,18 +81,18 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { } public func closePost(id: String) -> RxSwift.Single { - service.request(api: .closePost(id: id), with: .withToken) + recruitmentPostService.request(api: .closePost(id: id), with: .withToken) .mapToVoid() } public func removePost(id: String) -> RxSwift.Single { - service.request(api: .removePost(id: id), with: .withToken) + recruitmentPostService.request(api: .removePost(id: id), with: .withToken) .mapToVoid() } // MARK: Worker public func getPostDetailForWorker(id: String) -> RxSwift.Single { - service.request( + recruitmentPostService.request( api: .postDetail(id: id, userType: .worker), with: .withToken ) @@ -102,14 +104,16 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { public func getNativePostListForWorker(nextPageId: String?, requestCnt: Int = 10) -> RxSwift.Single { - service.request( + recruitmentPostService.request( api: .getOnGoingNativePostListForWorker(nextPageId: nextPageId, requestCnt: String(requestCnt)), with: .withToken ) .map(RecruitmentPostListForWorkerDTO.self) .catch({ error in if let moyaError = error as? MoyaError, case .objectMapping(let error, _) = moyaError { - print(error.localizedDescription) + #if DEBUG + print("앱용 공고 전체조회 에러:", error.localizedDescription) + #endif } return .error(error) }) @@ -117,6 +121,56 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { dto.toEntity() } } + + public func getFavoritePostListForWorker(nextPageId: String?, requestCnt: Int) -> RxSwift.Single { + recruitmentPostService.request( + api: .getFavoritePostListForWorker(nextPageId: nextPageId, requestCnt: String(requestCnt)), + with: .withToken + ) + .map(RecruitmentPostListForWorkerDTO.self) + .catch({ error in + if let moyaError = error as? MoyaError, case .objectMapping(let error, _) = moyaError { + #if DEBUG + print("즐겨찾기한 공고 전체조회 에러:",error.localizedDescription) + #endif + } + return .error(error) + }) + .map { dto in + dto.toEntity() + } + } + + public func getAppliedPostListForWorker(nextPageId: String?, requestCnt: Int) -> RxSwift.Single { + recruitmentPostService.request( + api: .getAppliedPostListForWorker(nextPageId: nextPageId, requestCnt: String(requestCnt)), + with: .withToken + ) + .map(RecruitmentPostListForWorkerDTO.self) + .catch({ error in + if let moyaError = error as? MoyaError, case .objectMapping(let error, _) = moyaError { + #if DEBUG + print("지원한 공고 전체조회 에러:", error.localizedDescription) + #endif + } + return .error(error) + }) + .map { dto in + dto.toEntity() + } + } + + public func ApplyToPost(postId: String, method: ApplyType) -> Single { + applyService + .request( + api: .applys( + jobPostingId: postId, + applyMethodType: method.dtoFormString + ), + with: .withToken + ) + .mapToVoid() + } } // MARK: 공고등록 정보를 DTO로 변환하는 영역 diff --git a/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserInfoLocalRepository.swift b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserInfoLocalRepository.swift index 6b8eaa95..a4a84054 100644 --- a/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserInfoLocalRepository.swift +++ b/project/Projects/Data/ConcreteRepository/UserInfo/DefaultUserInfoLocalRepository.swift @@ -21,6 +21,7 @@ enum UserInfoStorageKey: String, Hashable, CaseIterable { // Center case currentCenter = "currentCenter" + case currentCenterAuthState = "currentCenterAuthState" } public class DefaultUserInfoLocalRepository: UserInfoLocalRepository { @@ -83,6 +84,18 @@ public class DefaultUserInfoLocalRepository: UserInfoLocalRepository { localStorageService.saveData(key: K.currentCenter.rawValue, value: encoded) } + public func setCenterAuthState(state: CenterAuthState) { + localStorageService.saveData(key: K.currentCenterAuthState.rawValue, value: state.rawValue) + } + + public func getCenterAuthState() -> Entity.CenterAuthState? { + if let centerState: String = localStorageService.fetchData(key: K.currentCenterAuthState.rawValue) { + + return CenterAuthState(rawValue: centerState) + } + return nil + } + public func removeAllData() { UserInfoStorageKey.allCases.forEach { key in diff --git a/project/Projects/Data/DataSource/API/ApplyAPI.swift b/project/Projects/Data/DataSource/API/ApplyAPI.swift new file mode 100644 index 00000000..b0c92951 --- /dev/null +++ b/project/Projects/Data/DataSource/API/ApplyAPI.swift @@ -0,0 +1,60 @@ +// +// ApplyAPI.swift +// DataSource +// +// Created by choijunios on 9/3/24. +// + +import Foundation +import Moya +import Alamofire +import Entity + +public enum ApplyAPI { + case applys(jobPostingId: String, applyMethodType: String) +} + +extension ApplyAPI: BaseAPI { + + public var apiType: APIType { + .applys + } + + public var path: String { + switch self { + case .applys: + "" + } + } + + public var method: Moya.Method { + switch self { + case .applys: + .post + } + } + + var bodyParameters: Parameters? { + var params: Parameters = [:] + switch self { + case .applys(let jobPostingId, let applyMethodType): + params["jobPostingId"] = jobPostingId + params["applyMethodType"] = applyMethodType + return params + } + } + + var parameterEncoding: ParameterEncoding { + switch self { + default: + return JSONEncoding.default + } + } + + public var task: Moya.Task { + switch self { + case .applys: + return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) + } + } +} diff --git a/project/Projects/Data/DataSource/API/BaseAPI.swift b/project/Projects/Data/DataSource/API/BaseAPI.swift index 50c67249..a3d2fe31 100644 --- a/project/Projects/Data/DataSource/API/BaseAPI.swift +++ b/project/Projects/Data/DataSource/API/BaseAPI.swift @@ -14,6 +14,7 @@ public enum APIType { case users case job_postings case external(url: String) + case applys } // MARK: BaseAPI @@ -35,6 +36,8 @@ public extension BaseAPI { baseStr += "/users" case .job_postings: baseStr += "/job-postings" + case .applys: + baseStr += "/applys" case .external(let url): baseStr = url } diff --git a/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift b/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift index d7ce29ee..09b61406 100644 --- a/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift +++ b/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift @@ -29,9 +29,10 @@ public enum RcruitmentPostAPI { // - 공고 지원자 관련 case getPostApplicantCount(id: String) - // Worker case getOnGoingNativePostListForWorker(nextPageId: String?, requestCnt: String) + case getFavoritePostListForWorker(nextPageId: String?, requestCnt: String) + case getAppliedPostListForWorker(nextPageId: String?, requestCnt: String) } extension RcruitmentPostAPI: BaseAPI { @@ -70,6 +71,11 @@ extension RcruitmentPostAPI: BaseAPI { case .getOnGoingNativePostListForWorker: "" + case .getFavoritePostListForWorker: + "/my/favorites" + case .getAppliedPostListForWorker: + "/carer/my/applied" + } } @@ -103,6 +109,10 @@ extension RcruitmentPostAPI: BaseAPI { case .getOnGoingNativePostListForWorker: .get + case .getFavoritePostListForWorker: + .get + case .getAppliedPostListForWorker: + .get } } @@ -114,6 +124,16 @@ extension RcruitmentPostAPI: BaseAPI { params["next"] = nextPageId } params["limit"] = requestCnt + case .getFavoritePostListForWorker(let nextPageId, let requestCnt): + if let nextPageId { + params["next"] = nextPageId + } + params["limit"] = requestCnt + case .getAppliedPostListForWorker(let nextPageId, let requestCnt): + if let nextPageId { + params["next"] = nextPageId + } + params["limit"] = requestCnt default: break } @@ -122,7 +142,9 @@ extension RcruitmentPostAPI: BaseAPI { var parameterEncoding: ParameterEncoding { switch self { - case .getOnGoingNativePostListForWorker: + case .getOnGoingNativePostListForWorker, + .getFavoritePostListForWorker, + .getAppliedPostListForWorker: return URLEncoding.queryString default: return JSONEncoding.default @@ -131,7 +153,9 @@ extension RcruitmentPostAPI: BaseAPI { public var task: Moya.Task { switch self { - case .getOnGoingNativePostListForWorker: + case .getOnGoingNativePostListForWorker, + .getFavoritePostListForWorker, + .getAppliedPostListForWorker: .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) case .registerPost(let bodyData): .requestData(bodyData) diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift index 164df625..f215a1c8 100644 --- a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift @@ -54,16 +54,7 @@ public struct RecruitmentPostDTO: Codable { 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 + workTimeAndPay.paymentAmount = String(payAmount) let addressInfo: AddressInputStateObject = .init() addressInfo.addressInfo = .init( @@ -76,7 +67,7 @@ public struct RecruitmentPostDTO: Codable { let currentYear = Calendar.current.component(.year, from: Date()) customerInfo.birthYear = String(currentYear - age) - customerInfo.weight = (weight == nil) ? String(weight!) : "" + customerInfo.weight = (weight == nil) ? "" : String(weight!) customerInfo.careGrade = CareGrade(rawValue: careLevel-1)! customerInfo.cognitionState = CognitionDegree.toEntity(text: mentalStatus) diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift index 47ca3703..87211adf 100644 --- a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift @@ -40,32 +40,20 @@ public struct RecruitmentPostForWorkerDTO: Codable { public let applyDeadlineType: String public let applyDeadline: String? public let distance: Int + public let applyTime: String? + public let isFavorite: Bool - public func toEntity() -> RecruitmentPostForWorkerVO { + public func toEntity() -> NativeRecruitmentPostForWorkerVO { 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) - } + let deadlineDate = self.applyDeadline != nil ? dateFormatter.date(from: self.applyDeadline!) : nil + let applyDate = self.applyTime != nil ? dateFormatter.date(from: self.applyDeadline!) : nil return .init( postId: id, @@ -81,8 +69,10 @@ public struct RecruitmentPostForWorkerDTO: Codable { applyDeadlineType: ApplyDeadlineType.toEntity(text: applyDeadlineType), applyDeadlineDate: deadlineDate, payType: PaymentType.toEntity(text: payType), - payAmount: formedPayAmount, - distanceFromWorkPlace: distanceText + payAmount: String(payAmount), + distanceFromWorkPlace: distance, + applyTime: applyDate, + isFavorite: isFavorite ) } } diff --git a/project/Projects/Data/DataSource/Service/ApplyService.swift b/project/Projects/Data/DataSource/Service/ApplyService.swift new file mode 100644 index 00000000..487e19e7 --- /dev/null +++ b/project/Projects/Data/DataSource/Service/ApplyService.swift @@ -0,0 +1,17 @@ +// +// ApplyService.swift +// DataSource +// +// Created by choijunios on 9/3/24. +// + +import Foundation + +public class ApplyService: BaseNetworkService { + + public init() { } + + public override init(keyValueStore: KeyValueStore) { + super.init(keyValueStore: keyValueStore) + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift index 99fe5d6c..7001eb08 100644 --- a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift @@ -98,16 +98,80 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { let stream: Single! switch request { - case .native(let nextPageId): + case .initial: stream = repository.getNativePostListForWorker( - nextPageId: nextPageId, + nextPageId: nil, requestCnt: postCount ) - case .thirdParty(let nextPageId): - /// 워크넷 가져오기 미구현 - fatalError() + case .paging(let source, let nextPageId): + switch source { + case .native: + stream = repository.getNativePostListForWorker( + nextPageId: nextPageId, + requestCnt: postCount + ) + case .thirdParty: + // TODO: ‼️ ‼️워크넷 가져오기 미구현 + fatalError() + } + } + + return convert(task: stream) + } + + public func getFavoritePostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> RxSwift.Single> { + + let stream: Single! + + switch request { + case .initial: + stream = repository.getFavoritePostListForWorker( + nextPageId: nil, + requestCnt: postCount + ) + case .paging(let source, let nextPageId): + switch source { + case .native: + stream = repository.getFavoritePostListForWorker( + nextPageId: nextPageId, + requestCnt: postCount + ) + case .thirdParty: + // TODO: ‼️ ‼️워크넷 가져오기 미구현 + fatalError() + } } return convert(task: stream) } + + public func getAppliedPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> RxSwift.Single> { + + let stream: Single! + + switch request { + case .initial: + stream = repository.getAppliedPostListForWorker( + nextPageId: nil, + requestCnt: postCount + ) + case .paging(let source, let nextPageId): + switch source { + case .native: + stream = repository.getAppliedPostListForWorker( + nextPageId: nextPageId, + requestCnt: postCount + ) + case .thirdParty: + // TODO: ‼️ ‼️워크넷 가져오기 미구현 + fatalError() + } + } + + return convert(task: stream) + } + + public func applyToPost(postId: String, method: Entity.ApplyType) -> RxSwift.Single> { + convert(task: repository.ApplyToPost(postId: postId, method: method)) + } } diff --git a/project/Projects/Domain/Entity/State/Auth/Center/CenterAuthState.swift b/project/Projects/Domain/Entity/State/Auth/Center/CenterAuthState.swift new file mode 100644 index 00000000..1dd09f43 --- /dev/null +++ b/project/Projects/Domain/Entity/State/Auth/Center/CenterAuthState.swift @@ -0,0 +1,17 @@ +// +// CenterAuthState.swift +// Entity +// +// Created by choijunios on 8/29/24. +// + +import Foundation + +public enum CenterAuthState: String { + /// #1. 인증 요청이 되지 않은 상태입니다. + case notRequested + /// #2. 프로필이 입력되지 않은 상태입니다. + case noProfile + /// #3. 인증이 완료된 단계입니다. + case authFinished +} diff --git a/project/Projects/Domain/Entity/State/RecruitmentPost/Paging/PostPagingRequestForWorker.swift b/project/Projects/Domain/Entity/State/RecruitmentPost/Paging/PostPagingRequestForWorker.swift index e2c2602d..b73cdefb 100644 --- a/project/Projects/Domain/Entity/State/RecruitmentPost/Paging/PostPagingRequestForWorker.swift +++ b/project/Projects/Domain/Entity/State/RecruitmentPost/Paging/PostPagingRequestForWorker.swift @@ -7,7 +7,11 @@ import Foundation -public enum PostPagingRequestForWorker { - case native(nextPageId: String?) - case thirdParty(nextPageId: String?) +public enum PostPagingRequestForWorker: Equatable { + public enum Source { + case native + case thirdParty + } + case initial + case paging(source: Source, nextPageId: String?) } diff --git a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift b/project/Projects/Domain/Entity/VO/Employ/WorkerNativeEmployCardVO.swift similarity index 80% rename from project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift rename to project/Projects/Domain/Entity/VO/Employ/WorkerNativeEmployCardVO.swift index 658f24cc..cd2c4e34 100644 --- a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift +++ b/project/Projects/Domain/Entity/VO/Employ/WorkerNativeEmployCardVO.swift @@ -1,5 +1,5 @@ // -// WorkerEmployCardVO.swift +// WorkerNativeEmployCardVO.swift // Entity // // Created by choijunios on 7/19/24. @@ -7,11 +7,11 @@ import Foundation -public struct WorkerEmployCardVO { +public struct WorkerNativeEmployCardVO { public let dayLeft: Int public let isBeginnerPossible: Bool - public let distanceFromWorkPlace: String + public let distanceFromWorkPlace: Int public let title: String public let targetAge: Int public let careGrade: CareGrade @@ -21,11 +21,13 @@ public struct WorkerEmployCardVO { public let endTime: String public let paymentType: PaymentType public let paymentAmount: String + public let applyDate: Date? + public let isFavorite: Bool public init( dayLeft: Int, isBeginnerPossible: Bool, - distanceFromWorkPlace: String, + distanceFromWorkPlace: Int, title: String, targetAge: Int, careGrade: CareGrade, @@ -34,9 +36,10 @@ public struct WorkerEmployCardVO { startTime: String, endTime: String, paymentType: PaymentType, - paymentAmount: String + paymentAmount: String, + applyDate: Date?, + isFavorite: Bool ) { - self.dayLeft = dayLeft self.isBeginnerPossible = isBeginnerPossible self.distanceFromWorkPlace = distanceFromWorkPlace @@ -49,16 +52,18 @@ public struct WorkerEmployCardVO { self.endTime = endTime self.paymentType = paymentType self.paymentAmount = paymentAmount + self.applyDate = applyDate + self.isFavorite = isFavorite } - /// 서버가 입력중인 공고의 확인화면에 사용됩니다. + /// 공고 상세화면에서 사용됩니다. public static func create( workTimeAndPay: WorkTimeAndPayStateObject, customerRequirement: CustomerRequirementStateObject, customerInformation: CustomerInformationStateObject, applicationDetail: ApplicationDetailStateObject, addressInfo: AddressInputStateObject - ) -> WorkerEmployCardVO { + ) -> WorkerNativeEmployCardVO { // 남은 일수 var leftDay: Int? = nil @@ -101,10 +106,10 @@ public struct WorkerEmployCardVO { let paymentType = workTimeAndPay.paymentType ?? .hourly let paymentAmount = workTimeAndPay.paymentAmount - return WorkerEmployCardVO( + return WorkerNativeEmployCardVO( dayLeft: leftDay ?? 31, isBeginnerPossible: isBeginnerPossible, - distanceFromWorkPlace: "500", + distanceFromWorkPlace: 500, title: title, targetAge: targetAge, careGrade: careGrade, @@ -113,11 +118,13 @@ public struct WorkerEmployCardVO { startTime: startTime, endTime: workEndTime, paymentType: paymentType, - paymentAmount: paymentAmount + paymentAmount: paymentAmount, + applyDate: nil, + isFavorite: false ) } - public static func create(vo: RecruitmentPostForWorkerVO) -> WorkerEmployCardVO { + public static func create(vo: NativeRecruitmentPostForWorkerVO) -> WorkerNativeEmployCardVO { // 남은 일수 var leftDay: Int? = nil @@ -142,7 +149,9 @@ public struct WorkerEmployCardVO { startTime: vo.startTime, endTime: vo.endTime, paymentType: vo.payType, - paymentAmount: vo.payAmount + paymentAmount: vo.payAmount, + applyDate: vo.applyTime, + isFavorite: vo.isFavorite ) } } @@ -155,12 +164,12 @@ fileprivate extension String { } } -public extension WorkerEmployCardVO { +public extension WorkerNativeEmployCardVO { - static let mock = WorkerEmployCardVO( + static let mock: WorkerNativeEmployCardVO = .init( dayLeft: 10, isBeginnerPossible: true, - distanceFromWorkPlace: "500m", + distanceFromWorkPlace: 500, title: "서울특별시 강남구 신사동", targetAge: 78, careGrade: .four, @@ -169,13 +178,15 @@ public extension WorkerEmployCardVO { startTime: "09:00", endTime: "15:00", paymentType: .hourly, - paymentAmount: "12,500" + paymentAmount: "12,500", + applyDate: nil, + isFavorite: false ) - static let `default` = WorkerEmployCardVO( + static let `default`: WorkerNativeEmployCardVO = .init( dayLeft: 0, isBeginnerPossible: true, - distanceFromWorkPlace: "8km", + distanceFromWorkPlace: 8000, title: "기본값", targetAge: 10, careGrade: .one, @@ -184,6 +195,8 @@ public extension WorkerEmployCardVO { startTime: "00:00", endTime: "00:00", paymentType: .hourly, - paymentAmount: "12,500" + paymentAmount: "12,500", + applyDate: nil, + isFavorite: false ) } diff --git a/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift index 6ffcb8c1..9d366113 100644 --- a/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift +++ b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift @@ -9,18 +9,18 @@ import Foundation public struct RecruitmentPostListForWorkerVO { - public let posts: [RecruitmentPostForWorkerVO] + public let posts: [NativeRecruitmentPostForWorkerVO] public let nextPageId: String? public let fetchedPostCount: Int - public init(posts: [RecruitmentPostForWorkerVO], nextPageId: String?, fetchedPostCount: Int) { + public init(posts: [NativeRecruitmentPostForWorkerVO], nextPageId: String?, fetchedPostCount: Int) { self.posts = posts self.nextPageId = nextPageId self.fetchedPostCount = fetchedPostCount } } -public struct RecruitmentPostForWorkerVO { +public struct NativeRecruitmentPostForWorkerVO { public let postId: String public let workDays: [WorkDay] @@ -40,25 +40,11 @@ public struct RecruitmentPostForWorkerVO { public let payType: PaymentType public let payAmount: String - public let distanceFromWorkPlace: String + public let distanceFromWorkPlace: Int + public let applyTime: Date? + public let isFavorite: Bool - 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 - ) { + 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: Int, applyTime: Date?, isFavorite: Bool) { self.postId = postId self.workDays = workDays self.startTime = startTime @@ -74,9 +60,11 @@ public struct RecruitmentPostForWorkerVO { self.payType = payType self.payAmount = payAmount self.distanceFromWorkPlace = distanceFromWorkPlace + self.applyTime = applyTime + self.isFavorite = isFavorite } - public static let mock = RecruitmentPostForWorkerVO( + public static let mock = NativeRecruitmentPostForWorkerVO( postId: "test-post-id", workDays: [.mon, .wed, .fri], startTime: "09:00", @@ -90,7 +78,9 @@ public struct RecruitmentPostForWorkerVO { applyDeadlineType: .specificDate, applyDeadlineDate: Calendar.current.date(byAdding: .day, value: 7, to: Date()), payType: .hourly, - payAmount: "15,000", - distanceFromWorkPlace: "2.5km" + payAmount: "15000", + distanceFromWorkPlace: 2500, + applyTime: Date(), + isFavorite: true ) } diff --git a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift index ff3a9ece..fbdccd82 100644 --- a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift @@ -44,6 +44,15 @@ public protocol RecruitmentPostRepository: RepositoryBase { /// 요양보호사 공고의 상세정보를 조회합니다. func getPostDetailForWorker(id: String) -> Single - /// 요샹보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. + /// 요양보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. func getNativePostListForWorker(nextPageId: String?, requestCnt: Int) -> Single + + /// 요양보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. + func getFavoritePostListForWorker(nextPageId: String?, requestCnt: Int) -> Single + + /// 요양보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. + func getAppliedPostListForWorker(nextPageId: String?, requestCnt: Int) -> Single + + /// 요양보호사가 인앱 공고에 지원합니다. + func ApplyToPost(postId: String, method: ApplyType) -> Single } diff --git a/project/Projects/Domain/RepositoryInterface/UserInfo/UserInfoLocalRepository.swift b/project/Projects/Domain/RepositoryInterface/UserInfo/UserInfoLocalRepository.swift index b93c6983..c03addd1 100644 --- a/project/Projects/Domain/RepositoryInterface/UserInfo/UserInfoLocalRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/UserInfo/UserInfoLocalRepository.swift @@ -28,6 +28,15 @@ public protocol UserInfoLocalRepository { /// 로컬에 저장될 유저정보를 업데이트합니다. func updateCurrentCenterData(vo: CenterProfileVO) + /// 센터의 인증 정보를 설정합니다. + func setCenterAuthState(state: CenterAuthState) + + /// 센터의 인증 정보를 가져옵니다. + func getCenterAuthState() -> Entity.CenterAuthState? + + /// 센터의 인증 정보를 가져옵니다. +// func getCenterAuthState() -> CenterAuthState + /// 유저타입, 정보를 모두 삭제합니다. func removeAllData() } diff --git a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift index 917b5571..ca84ec1c 100644 --- a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift @@ -51,4 +51,13 @@ public protocol RecruitmentPostUseCase: UseCaseBase { /// 요양보호사가 메인화면에 사용할 공고리스트를 호출합니다. func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> + + /// 요양보호사가 즐겨찾기한 공고리스트를 호출합니다. + func getFavoritePostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> + + /// 요양보호사가 지원한 공고리스트를 호출합니다. + func getAppliedPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> + + /// 요양보호사가 인앱공고에 지원합니다. + func applyToPost(postId: String, method: ApplyType) -> Single> } diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift index 9cc654d7..9b3b4b50 100644 --- a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift @@ -26,7 +26,7 @@ class ViewController3: UIViewController { // tableView.estimatedRowHeight = 44.0 tableView.rowHeight = UITableView.automaticDimension - tableView.register(WorkerEmployCardCell.self, forCellReuseIdentifier: String(describing: WorkerEmployCardCell.self)) + tableView.register(WorkerNativeEmployCardCell.self, forCellReuseIdentifier: String(describing: WorkerNativeEmployCardCell.self)) [ tableView ] @@ -51,7 +51,7 @@ extension ViewController3: UITableViewDelegate, UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: WorkerEmployCardCell.self), for: indexPath) as? WorkerEmployCardCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: WorkerNativeEmployCardCell.self), for: indexPath) as? WorkerNativeEmployCardCell else { fatalError("Unable to dequeue WorkerEmployCard") } return cell diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift index 7b2eabbf..9d61f69d 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift @@ -59,14 +59,23 @@ public class DefaultIdleAlertVM: IdleAlertViewModelable { public class IdleBigAlertController: UIViewController { + public enum ButtonType { + case orange + case red + } + let customTranstionDelegate = CustomTransitionDelegate() + // Init + let type: ButtonType + // Not init private let disposeBag = DisposeBag() // View let titleLabel: IdleLabel = { let label = IdleLabel(typography: .Subtitle1) + label.numberOfLines = 0 label.textAlignment = .center return label }() @@ -74,25 +83,24 @@ public class IdleBigAlertController: UIViewController { let descriptionLabel: IdleLabel = { let label = IdleLabel(typography: .Body3) label.attrTextColor = DSKitAsset.Colors.gray500.color - label.lineBreakMode = .byWordWrapping - label.textAlignment = .center label.numberOfLines = 0 + label.textAlignment = .center return label }() - public let cancelButton: IdleThirdinaryButton = { + public lazy var cancelButton: IdleThirdinaryButton = { let button = IdleThirdinaryButton(level: .medium) button.label.textString = "" return button }() - public let acceptButton: IdlePrimaryButton = { - let button = IdlePrimaryButton(level: .mediumRed) + public lazy var acceptButton: IdlePrimaryButton = { + let button = IdlePrimaryButton(level: type == .orange ? .medium : .mediumRed) button.label.textString = "" return button }() - public init() { - + public init(type: ButtonType) { + self.type = type super.init(nibName: nil, bundle: nil) self.transitioningDelegate = customTranstionDelegate @@ -105,18 +113,17 @@ public class IdleBigAlertController: UIViewController { private func setAppearance() { view.backgroundColor = DSKitAsset.Colors.gray500.color.withAlphaComponent(0.5) - - // TODO: 미정으로 변동가능합니다. - view.layoutMargins = .init(top: 0, left: 24, bottom: 0, right: 24) } private func setAutoLayout() { + view.layoutMargins = .init(top: 0, left: 24, bottom: 0, right: 24) + // 라벨 스택 let textStack = VStack( [ - Spacer(height: 8), - titleLabel + titleLabel, + descriptionLabel ], spacing: 8, alignment: .center @@ -136,11 +143,7 @@ public class IdleBigAlertController: UIViewController { // 라벨 + 버튼 스택 let alertContentsStack = VStack( [ - HStack([ - Spacer(width: 41.5), - textStack, - Spacer(width: 41.5) - ], alignment: .fill), + textStack, buttonStack ], spacing: 24, @@ -150,7 +153,6 @@ public class IdleBigAlertController: UIViewController { NSLayoutConstraint.activate([ // 버튼 스택 높이 지정 buttonStack.heightAnchor.constraint(equalToConstant: 52), - ]) // 전체 스택 @@ -193,7 +195,9 @@ public class IdleBigAlertController: UIViewController { public func bind(viewModel vm: IdleAlertViewModelable) { titleLabel.textString = vm.title + titleLabel.textAlignment = .center descriptionLabel.textString = vm.description + descriptionLabel.textAlignment = .center acceptButton.label.textString = vm.acceptButtonLabelText cancelButton.label.textString = vm.cancelButtonLabelText @@ -218,13 +222,13 @@ public class IdleBigAlertController: UIViewController { } } -class FadeInAnimator: NSObject, UIViewControllerAnimatedTransitioning { +public class FadeInAnimator: NSObject, UIViewControllerAnimatedTransitioning { - func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.35 // 애니메이션 지속 시간 } - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromView = transitionContext.view(forKey: .from) else { return } let containerView = transitionContext.containerView @@ -245,13 +249,13 @@ class FadeInAnimator: NSObject, UIViewControllerAnimatedTransitioning { } } -class FadeOutAnimator: NSObject, UIViewControllerAnimatedTransitioning { +public class FadeOutAnimator: NSObject, UIViewControllerAnimatedTransitioning { - func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.35 // 애니메이션 지속 시간 } - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let toView = transitionContext.view(forKey: .to) else { return } let containerView = transitionContext.containerView @@ -271,13 +275,13 @@ class FadeOutAnimator: NSObject, UIViewControllerAnimatedTransitioning { } } -class CustomTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { +public class CustomTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { - func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return FadeInAnimator() // 우리가 만든 사용자 정의 애니메이터를 반환 } - func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { return FadeOutAnimator() } } @@ -286,5 +290,5 @@ class CustomTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate @available(iOS 17.0, *) #Preview("Preview", traits: .defaultLayout) { - IdleBigAlertController() + IdleBigAlertController(type: .orange) } 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 3ab19f13..255e2a94 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 @@ -1,5 +1,5 @@ // -// WorkerEmployCard.swift +// WorkerNativeEmployCardRO.swift // DSKit // // Created by choijunios on 7/19/24. @@ -10,7 +10,7 @@ import RxSwift import RxCocoa import Entity -public class WorkerEmployCardRO { +public class WorkerNativeEmployCardRO { let showBiginnerTag: Bool let showDayLeftTag: Bool @@ -22,19 +22,29 @@ public class WorkerEmployCardRO { 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) { + init( + showBiginnerTag: Bool, + showDayLeftTag: Bool, + dayLeftTagText: String?, + titleText: String, + distanceFromWorkPlaceText: String, + targetInfoText: String, + workDaysText: String, + workTimeText: String, + payText: String + ) { self.showBiginnerTag = showBiginnerTag self.showDayLeftTag = showDayLeftTag self.dayLeftTagText = dayLeftTagText self.titleText = titleText - self.distanceFromWorkPlaceText = distanceFromWorkPlace + self.distanceFromWorkPlaceText = distanceFromWorkPlaceText self.targetInfoText = targetInfoText self.workDaysText = workDaysText self.workTimeText = workTimeText self.payText = payText } - public static func create(vo: WorkerEmployCardVO) -> WorkerEmployCardRO { + public static func create(vo: WorkerNativeEmployCardVO) -> WorkerNativeEmployCardRO { var dayLeftTagText: String? = nil var showDayLeftTag: Bool = false @@ -69,12 +79,20 @@ public class WorkerEmployCardRO { } let addressTitle = splittedAddress.joined(separator: " ") + // distance는 미터단위입니다. + var distanceText: String = "\(vo.distanceFromWorkPlace)m" + + if vo.distanceFromWorkPlace >= 1000 { + let kilometers = Double(vo.distanceFromWorkPlace)/1000.0 + distanceText = String(format: "%.1fkm", kilometers) + } + return .init( showBiginnerTag: vo.isBeginnerPossible, showDayLeftTag: showDayLeftTag, dayLeftTagText: dayLeftTagText, titleText: addressTitle, - distanceFromWorkPlace: "\(vo.distanceFromWorkPlace)m", + distanceFromWorkPlaceText: distanceText, targetInfoText: targetInfoText, workDaysText: workDaysText, workTimeText: workTimeText, @@ -82,12 +100,12 @@ public class WorkerEmployCardRO { ) } - public static let `mock`: WorkerEmployCardRO = .init( + public static let `mock`: WorkerNativeEmployCardRO = .init( showBiginnerTag: true, showDayLeftTag: true, dayLeftTagText: "D-14", titleText: "사울시 강남동", - distanceFromWorkPlace: "1.1km", + distanceFromWorkPlaceText: "1.1km", targetInfoText: "1등급 54세 여성", workDaysText: "", workTimeText: "월, 화, 수", @@ -320,7 +338,7 @@ public class WorkerEmployCard: UIView { payLabel.typography = .Body2 } - public func bind(ro: WorkerEmployCardRO) { + public func bind(ro: WorkerNativeEmployCardRO) { beginnerTag.isHidden = !ro.showBiginnerTag dayLeftTag.isHidden = !ro.showDayLeftTag diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerNativeEmployCardCell.swift similarity index 57% rename from project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift rename to project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerNativeEmployCardCell.swift index 4277bc3a..0fd1aafe 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerNativeEmployCardCell.swift @@ -1,5 +1,5 @@ // -// WorkerEmployCard.swift +// WorkerNativeEmployCardCell.swift // DSKit // // Created by choijunios on 7/19/24. @@ -10,42 +10,26 @@ 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 enum PostAppliedState { + case applied + case notApplied } -public protocol WorkerEmployCardViewModelable { - - // Output - var renderObject: Driver? { get } - var applicationInformation: Driver? { get } +public protocol WorkerNativeEmployCardViewModelable: AnyObject { - // Input - var cardClicked: PublishRelay { get } - var applyButtonClicked: PublishRelay { get } + /// '지원하기' 버튼이 눌렸을 때, 공고 id를 전달합니다. + var applyButtonClicked: PublishRelay<(postId: String, postTitle: String)> { get } - /// true일 경우 즐겨 찾기에 등록됩니다. - var starButtonClicked: PublishRelay { get } + /// 공고상세보기 + func showPostDetail(id: String) } -public class WorkerEmployCardCell: UITableViewCell { +public class WorkerNativeEmployCardCell: UITableViewCell { - public static let identifier = String(describing: WorkerEmployCardCell.self) + public static let identifier = String(describing: WorkerNativeEmployCardCell.self) - var viewModel: WorkerEmployCardViewModelable? + var viewModel: WorkerNativeEmployCardViewModelable? private var disposables: [Disposable?]? public override func layoutSubviews() { @@ -59,7 +43,7 @@ public class WorkerEmployCardCell: UITableViewCell { let cardView = WorkerEmployCard() let applyButton: IdlePrimaryCardButton = { let btn = IdlePrimaryCardButton(level: .large) - btn.label.textString = "" + btn.label.textString = "지원하기" return btn }() @@ -114,45 +98,36 @@ public class WorkerEmployCardCell: UITableViewCell { ]) } - public func bind(viewModel: WorkerEmployCardViewModelable) { + public func bind(postId: String, vo: WorkerNativeEmployCardVO, viewModel: WorkerNativeEmployCardViewModelable) { - self.viewModel = viewModel + // 지원 여부 + if let appliedDate = vo.applyDate { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "지원완료 yyyy. MM. dd" + let applyButtonLabelString = dateFormatter.string(from: appliedDate) + applyButton.label.textString = applyButtonLabelString + applyButton.setEnabled(false) + } + + // 카드 컨텐츠 바인딩 + let cardRO = WorkerNativeEmployCardRO.create(vo: vo) + cardView.bind(ro: cardRO) // 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 + .subscribe(onNext: { [weak viewModel] _ in + viewModel?.showPostDetail(id: postId) + }), + + applyButton.rx.tap + .map({ _ in (postId, vo.title) }) .bind(to: viewModel.applyButtonClicked), - cardView - .starButton - .eventPublisher - .map { $0 == .accent } - .bind(to: viewModel.starButtonClicked), + //TODO: 즐겨찾기 구현예정 ] self.disposables = disposables diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Button/IdlePrimaryButton.swift b/project/Projects/Presentation/DSKit/Sources/Component/Button/IdlePrimaryButton.swift index 86ecba89..6092bb29 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/Button/IdlePrimaryButton.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Button/IdlePrimaryButton.swift @@ -102,9 +102,7 @@ public class IdlePrimaryButton: TappableUIView { private func setApearance() { self.layer.cornerRadius = 8 self.clipsToBounds = true - - // InitialSetting - backgroundColor = level.idleColor + self.backgroundColor = level.idleColor } private func setAutoLayout() { 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 index ee198bc6..843d9a28 100644 --- 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 @@ -47,8 +47,8 @@ public class PostDetailForWorkerCoodinator: ChildCoordinator { } public func start() { - let vc = PostDetailForWorkerVC() - let vm = PostDetailForWorkerVM( + let vc = NativePostDetailForWorkerVC() + let vm = NativePostDetailForWorkerVM( postId: postId, coordinator: self, recruitmentPostUseCase: recruitmentPostUseCase diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/PostDetailForWorkerVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/NativePostDetailForWorkerVC.swift similarity index 91% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/PostDetailForWorkerVC.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/NativePostDetailForWorkerVC.swift index 187c09a2..61f25986 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/PostDetailForWorkerVC.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/NativePostDetailForWorkerVC.swift @@ -1,5 +1,5 @@ // -// PostDetailForWorkerVC.swift +// NativePostDetailForWorkerVC.swift // BaseFeature // // Created by choijunios on 8/7/24. @@ -13,9 +13,9 @@ import Entity import DSKit /// 센토도 요양보호사가 보는 공고화면을 볼 수 있기 때문에 해당뷰를 BaseFeature에 구현하였습니다. -public class PostDetailForWorkerVC: BaseViewController { +public class NativePostDetailForWorkerVC: BaseViewController { - var viewModel: PostDetailForWorkerViewModelable? + var viewModel: NativePostDetailForWorkerViewModelable? // Init @@ -124,10 +124,12 @@ public class PostDetailForWorkerVC: BaseViewController { .disposed(by: disposeBag) } - public func bind(viewModel: PostDetailForWorkerViewModelable) { + public func bind(viewModel: NativePostDetailForWorkerViewModelable) { self.viewModel = viewModel + super.bind(viewModel: viewModel, disposeBag: disposeBag) + // Output viewModel .postForWorkerBundle? @@ -136,17 +138,16 @@ public class PostDetailForWorkerVC: BaseViewController { 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 - ) - ) + let cardVO: WorkerNativeEmployCardVO = .create( + workTimeAndPay: bundle.workTimeAndPay, + customerRequirement: bundle.customerRequirement, + customerInformation: bundle.customerInformation, + applicationDetail: bundle.applicationDetail, + addressInfo: bundle.addressInfo ) + let cardRO: WorkerNativeEmployCardRO = .create(vo: cardVO) + + contentView.cardView.bind(ro: cardRO) // 근무 조건 contentView.workConditionView.bind( @@ -197,6 +198,14 @@ public class PostDetailForWorkerVC: BaseViewController { .disposed(by: disposeBag) } + viewModel + .idleAlertVM? + .drive(onNext: { [weak self] vm in + self?.showIdleModal(type: .orange, viewModel: vm) + }) + .disposed(by: disposeBag) + + viewModel .alert? .drive(onNext: { [weak self] alertVO in 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 index 23e74079..bbc6d96f 100644 --- 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 @@ -43,13 +43,13 @@ public class WorkPlaceAndWorkerLocationView: VStack { public let mapViewBackGround: TappableUIView = { let view = TappableUIView() view.backgroundColor = DSColor.gray050.color + view.layer.cornerRadius = 8 + view.clipsToBounds = true 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 }() 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/NativePostDetailForWorkerVM.swift similarity index 63% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/PostDetailForWorkerVM.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift index ee16d246..a6cc969c 100644 --- 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/NativePostDetailForWorkerVM.swift @@ -1,5 +1,5 @@ // -// asd.swift +// NativePostDetailForWorkerVM.swift // BaseFeature // // Created by choijunios on 8/15/24. @@ -13,12 +13,13 @@ import PresentationCore import UseCaseInterface import DSKit -public protocol PostDetailForWorkerViewModelable { +public protocol NativePostDetailForWorkerViewModelable: DefaultLoadingVMable { // Output var postForWorkerBundle: Driver? { get } var locationInfo: Driver? { get } var alert: Driver? { get } + var idleAlertVM: Driver? { get } // Input var viewWillAppear: PublishRelay { get } @@ -29,7 +30,7 @@ public protocol PostDetailForWorkerViewModelable { var centerCardClicked: PublishRelay { get } } -public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { +public class NativePostDetailForWorkerVM: NativePostDetailForWorkerViewModelable { public weak var coordinator: PostDetailForWorkerCoodinator? @@ -37,13 +38,15 @@ public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { private let postId: String private let recruitmentPostUseCase: RecruitmentPostUseCase - + // Ouput public var postForWorkerBundle: RxCocoa.Driver? public var locationInfo: RxCocoa.Driver? - public var alert: RxCocoa.Driver? + public var showLoading: Driver? + public var dismissLoading: Driver? + public var idleAlertVM: Driver? - + // Input public var backButtonClicked: RxRelay.PublishRelay = .init() public var applyButtonClicked: RxRelay.PublishRelay = .init() public var startButtonClicked: RxRelay.PublishRelay = .init() @@ -62,6 +65,12 @@ public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { self.coordinator = coordinator self.recruitmentPostUseCase = recruitmentPostUseCase + + // MARK: 로딩 옵저버블 + var loadingStartObservables: [Observable] = [] + var loadingEndObservables: [Observable] = [] + + let getPostDetailResult = viewWillAppear .flatMap { [recruitmentPostUseCase] _ in recruitmentPostUseCase @@ -72,6 +81,17 @@ public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { let getPostDetailSuccess = getPostDetailResult.compactMap { $0.value } let getPostDetailFailure = getPostDetailResult.compactMap { $0.error } + let getPostDetailFailureAlert = getPostDetailFailure + .map { error in + AlertWithCompletionVO( + title: "공고 불러오기 실패", + message: error.message, + buttonInfo: [ + ("닫기", { [weak self] in self?.coordinator?.coordinatorDidFinish() }) + ] + ) + } + postForWorkerBundle = getPostDetailSuccess.asDriver(onErrorRecover: { _ in fatalError() }) // MARK: 센터, 워커 위치정보 @@ -104,22 +124,6 @@ public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { } .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 @@ -129,8 +133,48 @@ public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { .disposed(by: disposeBag) // 지원하기 버튼 클릭 + // MARK: 지원하기 + let applyRequest: PublishRelay = .init() + self.idleAlertVM = applyButtonClicked + .map { _ in + DefaultIdleAlertVM( + title: "공고에 지원하시겠어요?", + description: "", + acceptButtonLabelText: "지원하기", + cancelButtonLabelText: "취소하기") { + applyRequest.accept(()) + } + } + .asDriver(onErrorDriveWith: .never()) + + // 로딩 시작 + loadingStartObservables.append(applyRequest.map { _ in }) + + let applyRequestResult = applyRequest + .flatMap { [recruitmentPostUseCase] _ in + + // 리스트화면에서는 앱내 지원만 지원합니다. + return recruitmentPostUseCase + .applyToPost(postId: postId, method: .app) + } + .share() + + // 로딩 종료 + loadingEndObservables.append(applyRequestResult.map { _ in }) + + let applyRequestSuccess = applyRequestResult.compactMap { $0.value } + let applyRequestFailure = applyRequestResult.compactMap { $0.error } + + let applyRequestFailureAlert = applyRequestFailure + .map { error in + AlertWithCompletionVO( + title: "지원하기 실패", + message: error.message + ) + } - // 즐겨찾기 버튼 클릭 + // MARK: 즐겨찾기 + // 센터 프로필 조회 버튼클릭 centerCardClicked @@ -141,6 +185,26 @@ public class PostDetailForWorkerVM: PostDetailForWorkerViewModelable { self.coordinator?.showCenterProfileScreen(centerId: centerId) }) .disposed(by: disposeBag) + + + // MARK: Alert + alert = Observable + .merge( + getPostDetailFailureAlert, + applyRequestFailureAlert + ) + .asDriver(onErrorJustReturn: .default) + + + // MARK: 로딩 + showLoading = Observable + .merge(loadingStartObservables) + .asDriver(onErrorDriveWith: .never()) + + dismissLoading = Observable + .merge(loadingEndObservables) + .delay(.milliseconds(500), scheduler: MainScheduler.instance) + .asDriver(onErrorDriveWith: .never()) } // MARK: Test 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 b6bf9b04..6f16b5bd 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 @@ -8,8 +8,16 @@ import UIKit import Entity import DSKit +import RxSwift -open class BaseViewController: UIViewController { } +open class BaseViewController: UIViewController { + + // MARK: loading trigger + /// present애니메이션중 dismiss가 발동될 경우 충돌이 발생하여 예상치못한 동작유발 + private var isLoadingPresenting: Bool = false + private var loadingDimissionRequested: Bool = false + private var loadingVC: UIViewController? +} // MARK: Alert public extension BaseViewController { @@ -36,11 +44,64 @@ public extension BaseViewController { } func showIdleModal( + type: IdleBigAlertController.ButtonType = .red, viewModel: IdleAlertViewModelable ) { - let alertVC = IdleBigAlertController() + let alertVC = IdleBigAlertController(type: type) alertVC.bind(viewModel: viewModel) alertVC.modalPresentationStyle = .custom present(alertVC, animated: true, completion: nil) } + + func showDefaultLoadingScreen() { + + let vc = DefaultLoadingVC() + loadingVC = vc + vc.modalPresentationStyle = .custom + + isLoadingPresenting = true + present(vc, animated: true) { + + if self.loadingDimissionRequested { + DispatchQueue.main.async { [weak self] in + vc.dismiss(animated: true) { + self?.loadingVC = nil + } + self?.isLoadingPresenting = false + } + } else { + self.isLoadingPresenting = false + } + } + } + + func dismissDefaultLoadingScreen() { + if let loadingVC { + + if !isLoadingPresenting { + loadingVC.dismiss(animated: true) { + self.loadingVC = nil + } + } else { + loadingDimissionRequested = true + } + } + } + + func bind(viewModel: DefaultLoadingVMable, disposeBag: DisposeBag) { + + viewModel + .showLoading? + .drive(onNext: { [weak self] _ in + self?.showDefaultLoadingScreen() + }) + .disposed(by: disposeBag) + + viewModel + .dismissLoading? + .drive(onNext: { [weak self] _ in + self?.dismissDefaultLoadingScreen() + }) + .disposed(by: disposeBag) + } } diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Loading/DefaultLoadingVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Loading/DefaultLoadingVC.swift new file mode 100644 index 00000000..24768b81 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Loading/DefaultLoadingVC.swift @@ -0,0 +1,67 @@ +// +// DefaultLoadingVC.swift +// BaseFeature +// +// Created by choijunios on 9/3/24. +// + +import UIKit +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public protocol DefaultLoadingVMable { + + var showLoading: Driver? { get } + var dismissLoading: Driver? { get } +} + +public class DefaultLoadingVC: UIViewController { + + let customTranstionDelegate = CustomTransitionDelegate() + + // Init + + // View + private let loadingView: UIActivityIndicatorView = .init() + + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + self.transitioningDelegate = customTranstionDelegate + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + setAppearance() + setLayout() + + loadingView.startAnimating() + } + + private func setAppearance() { + view.backgroundColor = DSColor.gray050.color.withAlphaComponent(0.5) + } + + private func setLayout() { + [ + loadingView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } +} + 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 9b341542..a1113854 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 @@ -29,7 +29,7 @@ public protocol PostOverviewViewModelable: var postOverviewCoordinator: PostOverviewCoordinator? { get set } /// 공고등록에 성공한 경우 해당 이벤트를 전달 받습니다 - var workerEmployCardVO: Driver? { get } + var workerEmployCardVO: Driver? { get } /// 유효한 값을 가져옵니다. func fetchFromState() 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 b5483b8c..1f7696b5 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift @@ -19,7 +19,7 @@ public protocol PostDetailViewModelable: { // Output var applicantCountText: Driver? { get } - var workerEmployCardVO: Driver? { get } + var workerEmployCardVO: Driver? { get } var requestDetailFailure: Driver? { get } var showOptionSheet: Driver? { get } @@ -60,20 +60,20 @@ public class PostDetailForCenterVM: PostDetailViewModelable { // MARK: DetailVC Interaction public var applicantCount: Int? - public var workerEmployCardVO: RxCocoa.Driver? - public var requestDetailFailure: RxCocoa.Driver? - public var showOptionSheet: RxCocoa.Driver? - public var alert: RxCocoa.Driver? + public var workerEmployCardVO: Driver? + public var requestDetailFailure: Driver? + public var showOptionSheet: Driver? + public var alert: Driver? - public let postEditButtonClicked: RxRelay.PublishRelay = .init() - public let exitButtonClicked: RxRelay.PublishRelay = .init() - public let checkApplicationButtonClicked: RxRelay.PublishRelay = .init() - public let optionButtonClicked: RxRelay.PublishRelay = .init() - public let removePostButtonClicked: RxRelay.PublishRelay = .init() - public let closePostButtonClicked: RxRelay.PublishRelay = .init() - public let showAsWorkerButtonClicked: RxRelay.PublishRelay = .init() + public let postEditButtonClicked: PublishRelay = .init() + public let exitButtonClicked: PublishRelay = .init() + public let checkApplicationButtonClicked: PublishRelay = .init() + public let optionButtonClicked: PublishRelay = .init() + public let removePostButtonClicked: PublishRelay = .init() + public let closePostButtonClicked: PublishRelay = .init() + public let showAsWorkerButtonClicked: PublishRelay = .init() - public let viewWillAppear: RxRelay.PublishRelay = .init() + public let viewWillAppear: PublishRelay = .init() // MARK: fetched @@ -175,7 +175,7 @@ public class PostDetailForCenterVM: PostDetailViewModelable { fetched_applicationDetail.accept(bundle.applicationDetail) fetched_addressInfo.accept(bundle.addressInfo) - return WorkerEmployCardVO.create( + return WorkerNativeEmployCardVO.create( workTimeAndPay: fetched_workTimeAndPay.value, customerRequirement: fetched_customerRequirement.value, customerInformation: fetched_customerInformation.value, 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 c63f41b6..173d49e1 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift @@ -54,7 +54,7 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { public var registerButtonClicked: PublishRelay = .init() public var overViewWillAppear: RxRelay.PublishRelay = .init() - public let workerEmployCardVO: Driver? + public let workerEmployCardVO: Driver? // MARK: register request public var postRegistrationSuccess: Driver? @@ -385,7 +385,7 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { // MARK: ----- Over view ----- - workerEmployCardVO = Observable + workerEmployCardVO = Observable .create { [ editing_workTimeAndPay, editing_customerInformation, @@ -394,7 +394,7 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { editing_addressInfo ] emitter in - let vo = WorkerEmployCardVO.create( + let vo = WorkerNativeEmployCardVO.create( workTimeAndPay: editing_workTimeAndPay.value, customerRequirement: editing_customerRequirement.value, customerInformation: editing_customerInformation.value, diff --git a/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift b/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift index 828d360d..0639c7cc 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift @@ -70,4 +70,8 @@ public class WorkerSettingScreenCoordinator: ChildCoordinator { func startRemoveWorkerAccountFlow() { parent?.startRemoveWorkerAccountFlow() } + + func showMyProfileScreen() { + parent?.showMyProfileScreen() + } } diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/WorkerProfileViewController.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/WorkerProfileViewController.swift index 2572f87e..eaff3ba7 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/WorkerProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/WorkerProfileViewController.swift @@ -310,8 +310,7 @@ public class WorkerProfileViewController: DisposableViewController { // starButton, tagNameStack, humanInfoStack, - contactButtonContainer, - divider, + VStack([contactButtonContainer, divider], spacing: 24, alignment: .fill), employeeInfoTitleLabel, employeeInfoStack 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 index c0e7857a..a7a066e0 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift @@ -13,14 +13,6 @@ import RxSwift import Entity import DSKit -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 @@ -46,9 +38,9 @@ public class StarredAndAppliedVC: BaseViewController { } private var currentState: TabBarState = .applied - private let viewControllerDict: [TabBarState: WorkerStaticPostBoardVC] = [ - .applied : WorkerStaticPostBoardVC(), - .starred : WorkerStaticPostBoardVC() + private let viewControllerDict: [TabBarState: WorkerPagablePostBoardVC] = [ + .applied : WorkerPagablePostBoardVC(), + .starred : WorkerPagablePostBoardVC() ] // Init @@ -166,8 +158,8 @@ public class StarredAndAppliedVC: BaseViewController { } public func bind( - appliedPostVM: WorkerStaticPostBoardVMable, - starredPostVM: WorkerStaticPostBoardVMable + appliedPostVM: AppliedPostBoardVM, + starredPostVM: StarredPostBoardVM ) { viewControllerDict[.applied]?.bind(viewModel: appliedPostVM) diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/SubVC/WorkerPagablePostBoardVC.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/SubVC/WorkerPagablePostBoardVC.swift new file mode 100644 index 00000000..a77eb7de --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/SubVC/WorkerPagablePostBoardVC.swift @@ -0,0 +1,213 @@ +// +// WorkerPagablePostBoardVC.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 WorkerPagablePostBoardVC: BaseViewController { + + typealias Cell = WorkerNativeEmployCardCell + + var viewModel: WorkerPagablePostBoardVMable? + + // View + 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 cellData: BehaviorRelay<[PostBoardCellData]> = .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() + 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: WorkerPagablePostBoardVMable) { + + self.viewModel = viewModel + + // 로딩 바인딩 + super.bind(viewModel: viewModel, disposeBag: disposeBag) + + // Output + viewModel + .postBoardData? + .drive(onNext: { [weak self] (isRefreshed, cellData) in + guard let self else { return } + self.cellData.accept(cellData) + self.postTableView.reloadData() + isPaging = false + + if isRefreshed { + DispatchQueue.main.async { [weak self] in + self?.postTableView.setContentOffset(.zero, animated: false) + } + } + }) + .disposed(by: disposeBag) + + viewModel + .alert? + .drive(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + + // Input + Observable + .merge(self.rx.viewWillAppear.map { _ in () }) + .bind(to: viewModel.requestInitialPageRequest) + .disposed(by: disposeBag) + + self.requestNextPage + .bind(to: viewModel.requestNextPage) + .disposed(by: disposeBag) + } + + func bind(viewModel: WorkerAppliablePostBoardVMable) { + + self.viewModel = viewModel + + // 로딩 바인딩 + super.bind(viewModel: viewModel, disposeBag: disposeBag) + + // Output + viewModel + .postBoardData? + .drive(onNext: { [weak self] (isRefreshed, cellData) in + guard let self else { return } + self.cellData.accept(cellData) + self.postTableView.reloadData() + isPaging = false + + if isRefreshed { + postTableView.setContentOffset(.zero, animated: false) + } + }) + .disposed(by: disposeBag) + + viewModel + .alert? + .drive(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + + // Input + Observable + .merge(self.rx.viewWillAppear.map { _ in () }) + .bind(to: viewModel.requestInitialPageRequest) + .disposed(by: disposeBag) + + self.requestNextPage + .bind(to: viewModel.requestNextPage) + .disposed(by: disposeBag) + } +} + +extension WorkerPagablePostBoardVC: UITableViewDataSource, UITableViewDelegate { + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + cellData.value.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell + cell.selectionStyle = .none + + let cellData = cellData.value[indexPath.row] + + if let vm = viewModel { + cell.bind(postId: cellData.postId, vo: cellData.cardVO, viewModel: vm) + } + + return cell + } +} + +// MARK: ScrollView관련 +extension WorkerPagablePostBoardVC { + 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(()) + } + } + } +} 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 deleted file mode 100644 index 6fe0db72..00000000 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/SubVC/WorkerStaticPostBoardVC.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// 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 index 1189da2d..9405887c 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVC.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVC.swift @@ -15,7 +15,7 @@ import DSKit public class WorkerRecruitmentPostBoardVC: BaseViewController { - typealias Cell = WorkerEmployCardCell + typealias Cell = WorkerNativeEmployCardCell var viewModel: WorkerRecruitmentPostBoardVMable? @@ -36,7 +36,7 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { var isPaging = true // Observable - let postViewModels: BehaviorRelay<[WorkerEmployCardViewModelable]> = .init(value: []) + let cellData: BehaviorRelay<[PostBoardCellData]> = .init(value: []) let requestNextPage: PublishRelay = .init() private let disposeBag = DisposeBag() @@ -57,6 +57,8 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { self.viewModel = viewModel + super.bind(viewModel: viewModel, disposeBag: disposeBag) + // Output viewModel .workerLocationTitleText? @@ -67,11 +69,17 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { viewModel .postBoardData? - .drive(onNext: { [weak self] viewModels in + .drive(onNext: { [weak self] (isRefreshed: Bool, cellData) in guard let self else { return } - self.postViewModels.accept(viewModels) - self.postTableView.reloadData() - self.isPaging = false + self.cellData.accept(cellData) + postTableView.reloadData() + isPaging = false + + if isRefreshed { + DispatchQueue.main.async { [weak self] in + self?.postTableView.setContentOffset(.zero, animated: false) + } + } }) .disposed(by: disposeBag) @@ -82,9 +90,21 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { }) .disposed(by: disposeBag) + viewModel + .idleAlertVM? + .drive(onNext: { [weak self] vm in + self?.showIdleModal(type: .orange, viewModel: vm) + }) + .disposed(by: disposeBag) + // Input self.rx.viewDidLoad - .bind(to: viewModel.viewDidLoad) + .bind(to: viewModel.requestWorkerLocation) + .disposed(by: disposeBag) + + self.rx.viewWillAppear + .map { _ in () } + .bind(to: viewModel.requestInitialPageRequest) .disposed(by: disposeBag) self.requestNextPage @@ -136,7 +156,7 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { extension WorkerRecruitmentPostBoardVC: UITableViewDataSource, UITableViewDelegate { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - postViewModels.value.count + cellData.value.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -144,8 +164,12 @@ extension WorkerRecruitmentPostBoardVC: UITableViewDataSource, UITableViewDelega let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell cell.selectionStyle = .none - let vm = postViewModels.value[indexPath.row] - cell.bind(viewModel: vm) + let cellData = cellData.value[indexPath.row] + + if let vm = viewModel { + + cell.bind(postId: cellData.postId, vo: cellData.cardVO, viewModel: vm) + } return cell } diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift index e0b4a477..ea9f80ef 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift @@ -15,51 +15,124 @@ import DSKit import UseCaseInterface -public class AppliedPostBoardVM: WorkerStaticPostBoardVMable { +public class AppliedPostBoardVM: WorkerPagablePostBoardVMable { - public var postViewWillAppear: RxRelay.PublishRelay = .init() + // Input + public var requestInitialPageRequest: RxRelay.PublishRelay = .init() + public var requestNextPage: RxRelay.PublishRelay = .init() + public var applyButtonClicked: RxRelay.PublishRelay<(postId: String, postTitle: String)> = .init() - public var postBoardData: RxCocoa.Driver<[any DSKit.WorkerEmployCardViewModelable]>? + // Output + public var postBoardData: RxCocoa.Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? public var alert: RxCocoa.Driver? + public var showLoading: RxCocoa.Driver? + public var dismissLoading: RxCocoa.Driver? // Init weak var coordinator: WorkerRecruitmentBoardCoordinatable? let recruitmentPostUseCase: RecruitmentPostUseCase - public init(recruitmentPostUseCase: RecruitmentPostUseCase) { + // Paging + /// 값이 nil이라면 요청을 보내지 않습니다. + var nextPagingRequest: PostPagingRequestForWorker? + /// 가장최신의 데이터를 홀드, 다음 요청시 해당데이터에 새로운 데이터를 더해서 방출 + private let currentPostVO: BehaviorRelay<[NativeRecruitmentPostForWorkerVO]> = .init(value: []) + + public init(coordinator: WorkerRecruitmentBoardCoordinatable, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.coordinator = coordinator self.recruitmentPostUseCase = recruitmentPostUseCase + self.nextPagingRequest = .initial - let requestPostResult = postViewWillAppear - .flatMap { [unowned self] _ in - self.publishAppliedPostMocks() + var loadingStartObservables: [Observable] = [] + var loadingEndObservables: [Observable] = [] + + // MARK: 공고리스트 처음부터 요청하기 + let initialRequest = requestInitialPageRequest + .flatMap { [weak self, recruitmentPostUseCase] request in + + self?.currentPostVO.accept([]) + self?.nextPagingRequest = .initial + + return recruitmentPostUseCase + .getAppliedPostListForWorker( + request: .initial, + postCount: 10 + ) } .share() - let requestPostSuccess = requestPostResult.compactMap { $0.value } - let requestPostFailure = requestPostResult.compactMap { $0.error } + // 로딩 시작 + loadingStartObservables.append(initialRequest.map { _ in }) + + // MARK: 공고리스트 페이징 요청 + let pagingRequest = requestNextPage + .compactMap { [weak self] _ in + // 요청이 없는 경우 요청을 보내지 않는다. + // ThirdPatry에서도 불러올 데이터가 없는 경우입니다. + self?.nextPagingRequest + } + .flatMap { [recruitmentPostUseCase] request in + recruitmentPostUseCase + .getAppliedPostListForWorker( + request: request, + postCount: 10 + ) + } + + let postPageReqeustResult = Observable + .merge(initialRequest, pagingRequest) + .share() + + // 로딩 종료 + loadingEndObservables.append(postPageReqeustResult.map { _ in }) + + let requestPostListSuccess = postPageReqeustResult.compactMap { $0.value } + let requestPostListFailure = postPageReqeustResult.compactMap { $0.error } - postBoardData = requestPostSuccess - .map { postForWorkerVos in + postBoardData = Observable + .zip( + currentPostVO, + requestPostListSuccess + ) + .compactMap { [weak self] (prevPostList, fetchedData) -> (Bool, [PostBoardCellData])? 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 + guard let self else { return nil } + + let isRefreshed: Bool = self.nextPagingRequest == .initial + + + if let next = fetchedData.nextPageId { + // 지원 공고의 경우 써드파티에서 불러올 데이터가 없다. + self.nextPagingRequest = .paging( + source: .native, + nextPageId: next ) + } else { + self.nextPagingRequest = nil + } + + + // 화면에 표시할 전체리스트 도출 + let fetchedPosts = fetchedData.posts + var mergedPosts = currentPostVO.value + mergedPosts.append(contentsOf: fetchedPosts) + + // 최근값 업데이트 + self.currentPostVO.accept(mergedPosts) + + // cellData 생성 + let cellData: [PostBoardCellData] = mergedPosts.map { vo in + + let cardVO: WorkerNativeEmployCardVO = .create(vo: vo) - return vm + return .init(postId: vo.postId, cardVO: cardVO) } - return viewModels + return (isRefreshed, cellData) } - .asDriver(onErrorJustReturn: []) + .asDriver(onErrorDriveWith: .never()) - alert = requestPostFailure + alert = requestPostListFailure .map { error in DefaultAlertContentVO( title: "지원한 공고 불러오기 오류", @@ -67,80 +140,19 @@ public class AppliedPostBoardVM: WorkerStaticPostBoardVMable { ) } .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) + // MARK: 로딩 + showLoading = Observable + .merge(loadingStartObservables) + .asDriver(onErrorDriveWith: .never()) - starButtonClicked - .subscribe(onNext: { [weak self] _ in - guard let self else { return } - - // 즐겨찾기 버튼눌림 - }) - .disposed(by: disposeBag) + dismissLoading = Observable + .merge(loadingEndObservables) + .delay(.milliseconds(500), scheduler: MainScheduler.instance) + .asDriver(onErrorDriveWith: .never()) + } + + public func showPostDetail(id: String) { + coordinator?.showPostDetail(postId: id) } } 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 index 9f8a5dab..7d216bff 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/OnGoingPostBoard/WorkerRecruitmentPostBoardVM.swift @@ -14,33 +14,58 @@ import Entity import DSKit import UseCaseInterface -public protocol WorkerRecruitmentPostBoardVMable: DefaultAlertOutputable { - +public struct PostBoardCellData { + let postId: String + let cardVO: WorkerNativeEmployCardVO +} + +/// 페이징 보드 +public protocol WorkerPagablePostBoardVMable: DefaultAlertOutputable & WorkerNativeEmployCardViewModelable & DefaultLoadingVMable { /// 다음 페이지를 요청합니다. var requestNextPage: PublishRelay { get } - /// ViewDidLoad - var viewDidLoad: PublishRelay { get } + /// 화면이 등장할 때마다 리스트를 초기화합니다. + var requestInitialPageRequest: PublishRelay { get } /// 페이지요청에 대한 결과를 전달합니다. - var postBoardData: Driver<[WorkerEmployCardViewModelable]>? { get } + var postBoardData: Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? { get } +} + +/// 페이징 + 지원하기 +public protocol WorkerAppliablePostBoardVMable: WorkerPagablePostBoardVMable { + /// 지원하기 Alert + var idleAlertVM: Driver? { get } +} + +/// 페이징 + 지원하기 + 요양보호사 위치정보 +public protocol WorkerRecruitmentPostBoardVMable: WorkerAppliablePostBoardVMable { + + /// 요양보호사 위치정보를 요청합니다. + var requestWorkerLocation: PublishRelay { get } + /// 요양보호사 위치 정보를 전달합니다. var workerLocationTitleText: Driver? { get } } public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { + + // Output - public var postBoardData: Driver<[WorkerEmployCardViewModelable]>? - public var alert: Driver? + public var postBoardData: Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? public var workerLocationTitleText: Driver? + public var idleAlertVM: RxCocoa.Driver? - + // Default + public var alert: Driver? + public var showLoading: Driver? + public var dismissLoading: Driver? // Input - public var viewDidLoad: PublishRelay = .init() + public var requestInitialPageRequest: PublishRelay = .init() + public var requestWorkerLocation: PublishRelay = .init() public var requestNextPage: PublishRelay = .init() - + public var applyButtonClicked: PublishRelay<(postId: String, postTitle: String)> = .init() // Init @@ -49,9 +74,9 @@ public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { // Paging /// 값이 nil이라면 요청을 보내지 않습니다. - var nextPagingRequest: PostPagingRequestForWorker? + var nextPagingRequest: PostPagingRequestForWorker? = .initial /// 가장최신의 데이터를 홀드, 다음 요청시 해당데이터에 새로운 데이터를 더해서 방출 - private let currentPostVO: BehaviorRelay<[RecruitmentPostForWorkerVO]> = .init(value: []) + private let currentPostVO: BehaviorRelay<[NativeRecruitmentPostForWorkerVO]> = .init(value: []) // Observable let dispostBag = DisposeBag() @@ -63,27 +88,89 @@ public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { { self.coordinator = coordinator self.recruitmentPostUseCase = recruitmentPostUseCase - self.nextPagingRequest = .native(nextPageId: nil) - // 상단 위치정보 - workerLocationTitleText = viewDidLoad + var loadingStartObservables: [Observable] = [] + var loadingEndObservables: [Observable] = [] + + // MARK: 상단 위치정보 불러오기 + workerLocationTitleText = requestWorkerLocation .compactMap { [weak self] _ in self?.fetchWorkerLocation() } - .asDriver(onErrorJustReturn: "반갑습니다.") + .asDriver(onErrorJustReturn: "위치정보확인불가") + // MARK: 지원하기 + let applyRequest: PublishRelay = .init() + self.idleAlertVM = applyButtonClicked + .map { (postId: String, postTitle: String) in + DefaultIdleAlertVM( + title: "'\(postTitle)'\n공고에 지원하시겠어요?", + description: "", + acceptButtonLabelText: "지원하기", + cancelButtonLabelText: "취소하기") { [applyRequest] in + applyRequest.accept(postId) + } + } + .asDriver(onErrorDriveWith: .never()) + + // 로딩 시작 + loadingStartObservables.append(applyRequest.map { _ in }) - let postPageReqeustResult = Observable - .merge( - viewDidLoad.asObservable(), - requestNextPage.asObservable() - ) + let applyRequestResult = applyRequest + .flatMap { [recruitmentPostUseCase] postId in + + // 리스트화면에서는 앱내 지원만 지원합니다. + return recruitmentPostUseCase + .applyToPost(postId: postId, method: .app) + } + .share() + + // 로딩 종료 + loadingEndObservables.append(applyRequestResult.map { _ in }) + + let applyRequestSuccess = applyRequestResult.compactMap { $0.value } + + // 지원하기 성공시 새로고침 + applyRequestSuccess + .bind(to: requestInitialPageRequest) + .disposed(by: dispostBag) + + let applyRequestFailure = applyRequestResult.compactMap { $0.error } + + let applyRequestFailureAlert = applyRequestFailure + .map { error in + DefaultAlertContentVO( + title: "지원하기 실패", + message: error.message + ) + } + + + // MARK: 공고리스트 처음부터 요청하기 + let initialRequest = requestInitialPageRequest + .flatMap { [weak self, recruitmentPostUseCase] request in + + self?.currentPostVO.accept([]) + self?.nextPagingRequest = .initial + + return recruitmentPostUseCase + .getPostListForWorker( + request: .initial, + postCount: 10 + ) + } + .share() + + // 로딩 시작 + loadingStartObservables.append(initialRequest.map { _ in }) + + // MARK: 공고리스트 페이징 요청 + let pagingRequest = requestNextPage .compactMap { [weak self] _ in // 요청이 없는 경우 요청을 보내지 않는다. // ThirdPatry에서도 불러올 데이터가 없는 경우입니다. self?.nextPagingRequest } - .share() .flatMap { [recruitmentPostUseCase] request in recruitmentPostUseCase .getPostListForWorker( @@ -91,8 +178,13 @@ public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { postCount: 10 ) } + + let postPageReqeustResult = Observable + .merge(initialRequest, pagingRequest) .share() - + + // 로딩 종료 + loadingEndObservables.append(postPageReqeustResult.map { _ in }) let requestPostListSuccess = postPageReqeustResult.compactMap { $0.value } let requestPostListFailure = postPageReqeustResult.compactMap { $0.error } @@ -102,10 +194,12 @@ public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { currentPostVO, requestPostListSuccess ) - .compactMap { [weak self] (prevPostList, fetchedData) -> [WorkerEmployCardViewModelable]? in + .compactMap { [weak self] (prevPostList, fetchedData) -> (Bool, [PostBoardCellData])? in guard let self else { return nil } + let isRefreshed: Bool = self.nextPagingRequest == .initial + // 다음 요청설정 var nextRequest: PostPagingRequestForWorker? if let prevRequest = self.nextPagingRequest { @@ -113,19 +207,26 @@ public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { if let nextPageId = fetchedData.nextPageId { // 다음값이 있는 경우 switch prevRequest { - case .native: - nextRequest = .native(nextPageId: nextPageId) - case .thirdParty: - nextRequest = .thirdParty(nextPageId: nextPageId) + case .initial: + nextRequest = .paging(source: .native, nextPageId: nextPageId) + case .paging(let source, let nextPageId): + nextRequest = .paging(source: source, nextPageId: nextPageId) } } else { // 다음값이 없는 경우 switch prevRequest { - case .native: - nextRequest = .thirdParty(nextPageId: nil) - case .thirdParty: - // 페이징 종료 - nextRequest = nil + case .initial: + // 써드파티 데이터 호출 + nextRequest = .paging(source: .thirdParty, nextPageId: nil) + case .paging(let source, _): + switch source { + case .native: + // 써드파티 데이터 호출 + nextRequest = .paging(source: .thirdParty, nextPageId: nil) + case .thirdParty: + // 페이징 종료 + nextRequest = nil + } } } } @@ -139,32 +240,45 @@ public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { // 최근값 업데이트 self.currentPostVO.accept(mergedPosts) - // ViewModel 생성 - let viewModels = mergedPosts.map { vo in - - let cardVO: WorkerEmployCardVO = .create(vo: vo) + // cellData생성 + let cellData: [PostBoardCellData] = mergedPosts.map { postVO in - let vm: OngoindWorkerEmployCardVM = .init( - postId: vo.postId, - vo: cardVO, - coordinator: self.coordinator - ) - - return vm + let cardVO: WorkerNativeEmployCardVO = .create(vo: postVO) + return .init(postId: postVO.postId, cardVO: cardVO) } - return viewModels + return (isRefreshed, cellData) } - .asDriver(onErrorJustReturn: []) + .asDriver(onErrorDriveWith: .never()) - alert = requestPostListFailure + let requestPostListFailureAlert = requestPostListFailure .map { error in - return DefaultAlertContentVO( - title: "시스템 오류", + DefaultAlertContentVO( + title: "공고 불러오기 오류", message: error.message ) } + + self.alert = Observable + .merge( + applyRequestFailureAlert, + requestPostListFailureAlert + ) .asDriver(onErrorJustReturn: .default) + + // MARK: 로딩 + showLoading = Observable + .merge(loadingStartObservables) + .asDriver(onErrorDriveWith: .never()) + + dismissLoading = Observable + .merge(loadingEndObservables) + .delay(.milliseconds(500), scheduler: MainScheduler.instance) + .asDriver(onErrorDriveWith: .never()) + } + + public func showPostDetail(id: String) { + coordinator?.showPostDetail(postId: id) } /// Test @@ -172,68 +286,3 @@ public class WorkerRecruitmentPostBoardVM: WorkerRecruitmentPostBoardVMable { "서울시 영등포구" } } - -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 index ab4c359f..88e73cd5 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift @@ -14,132 +14,186 @@ import Entity import DSKit import UseCaseInterface -public class StarredPostBoardVM: WorkerStaticPostBoardVMable { +public class StarredPostBoardVM: WorkerAppliablePostBoardVMable { - public var postViewWillAppear: RxRelay.PublishRelay = .init() + // Input + public var requestInitialPageRequest: RxRelay.PublishRelay = .init() + public var requestNextPage: RxRelay.PublishRelay = .init() + public var applyButtonClicked: RxRelay.PublishRelay<(postId: String, postTitle: String)> = .init() - public var postBoardData: RxCocoa.Driver<[any DSKit.WorkerEmployCardViewModelable]>? - public var alert: RxCocoa.Driver? + // Output + public var postBoardData: RxCocoa.Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? + public var alert: Driver? + public var idleAlertVM: RxCocoa.Driver? + public var showLoading: RxCocoa.Driver? + public var dismissLoading: RxCocoa.Driver? // Init weak var coordinator: WorkerRecruitmentBoardCoordinatable? let recruitmentPostUseCase: RecruitmentPostUseCase - public init(recruitmentPostUseCase: RecruitmentPostUseCase) { + // Paging + /// 값이 nil이라면 요청을 보내지 않습니다. + var nextPagingRequest: PostPagingRequestForWorker? + /// 가장최신의 데이터를 홀드, 다음 요청시 해당데이터에 새로운 데이터를 더해서 방출 + private let currentPostVO: BehaviorRelay<[NativeRecruitmentPostForWorkerVO]> = .init(value: []) + + public init(coordinator: WorkerRecruitmentBoardCoordinatable, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.coordinator = coordinator self.recruitmentPostUseCase = recruitmentPostUseCase + self.nextPagingRequest = .initial + + var loadingStartObservables: [Observable] = [] + var loadingEndObservables: [Observable] = [] + + // MARK: 공고리스트 처음부터 요청하기 + let initialRequest = requestInitialPageRequest + .flatMap { [weak self, recruitmentPostUseCase] request in + + self?.currentPostVO.accept([]) + self?.nextPagingRequest = .initial + + return recruitmentPostUseCase + .getFavoritePostListForWorker( + request: .initial, + postCount: 10 + ) + } + .share() + + // 로딩 시작 + loadingStartObservables.append(initialRequest.map { _ in }) - let requestPostResult = postViewWillAppear - .flatMap { [unowned self] _ in - self.publishStarredPostMocks() + // MARK: 공고리스트 페이징 요청 + let pagingRequest = requestNextPage + .compactMap { [weak self] _ in + // 요청이 없는 경우 요청을 보내지 않는다. + // ThirdPatry에서도 불러올 데이터가 없는 경우입니다. + self?.nextPagingRequest + } + .flatMap { [recruitmentPostUseCase] request in + recruitmentPostUseCase + .getFavoritePostListForWorker( + request: request, + postCount: 10 + ) } + + let postPageReqeustResult = Observable + .merge(initialRequest, pagingRequest) .share() - let requestPostSuccess = requestPostResult.compactMap { $0.value } - let requestPostFailure = requestPostResult.compactMap { $0.error } + // 로딩 종료 + loadingEndObservables.append(postPageReqeustResult.map { _ in }) - postBoardData = requestPostSuccess - .map { postForWorkerVos in + let requestPostListSuccess = postPageReqeustResult.compactMap { $0.value } + let requestPostListFailure = postPageReqeustResult.compactMap { $0.error } + + postBoardData = Observable + .zip( + currentPostVO, + requestPostListSuccess + ) + .compactMap { [weak self] (prevPostList, fetchedData) -> (Bool, [PostBoardCellData])? 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 + guard let self else { return nil } + + let isRefreshed: Bool = self.nextPagingRequest == .initial + + // TODO: ‼️ ‼️ 즐겨찾기 공고의 경우 서버에서 아직 워크넷 공고를 처리하는 방법을 정하지 못했음으로 추후에 수정할 예정입니다. + + if let next = fetchedData.nextPageId { + // 지원 공고의 경우 써드파티에서 불러올 데이터가 없다. + self.nextPagingRequest = .paging( + source: .native, + nextPageId: next ) + } else { + self.nextPagingRequest = nil + } + + // 화면에 표시할 전체리스트 도출 + let fetchedPosts = fetchedData.posts + var mergedPosts = currentPostVO.value + mergedPosts.append(contentsOf: fetchedPosts) + + // 최근값 업데이트 + self.currentPostVO.accept(mergedPosts) + + // cellData 생성 + let cellData: [PostBoardCellData] = mergedPosts.map { vo in + + let cardVO: WorkerNativeEmployCardVO = .create(vo: vo) - return vm + return .init(postId: vo.postId, cardVO: cardVO) } - return viewModels + return (isRefreshed, cellData) } - .asDriver(onErrorJustReturn: []) + .asDriver(onErrorDriveWith: .never()) - alert = requestPostFailure + // MARK: 지원하기 + let applyRequest: PublishRelay = .init() + self.idleAlertVM = applyButtonClicked + .map { (postId: String, postTitle: String) in + DefaultIdleAlertVM( + title: "'postTitle'\n공고에 지원하시겠어요?", + description: "", + acceptButtonLabelText: "지원하기", + cancelButtonLabelText: "취소하기") { [applyRequest] in + applyRequest.accept(postId) + } + } + .asDriver(onErrorDriveWith: .never()) + + // 로딩 시작 + loadingStartObservables.append(applyRequest.map { _ in }) + + let applyRequestResult = applyRequest + .flatMap { [recruitmentPostUseCase] postId in + // 리스트화면에서는 앱내 지원만 지원합니다. + recruitmentPostUseCase + .applyToPost(postId: postId, method: .app) + } + .share() + + // 로딩 종료 + loadingEndObservables.append(applyRequestResult.map { _ in }) + + let applyRequestFailure = applyRequestResult.compactMap { $0.error } + + let applyRequestFailureAlert = applyRequestFailure .map { error in DefaultAlertContentVO( - title: "즐겨찾기한 공고 불러오기 오류", + 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)) + let requestPostListFailureAlert = requestPostListFailure + .map { error in + DefaultAlertContentVO( + title: "즐겨찾기한 공고 불러오기 오류", + message: error.message + ) + } - // MARK: 버튼 처리 - applyButtonClicked - .subscribe(onNext: { [weak self] _ in - guard let self else { return } - - // 지원하기 버튼 눌림 - }) - .disposed(by: disposeBag) + alert = Observable + .merge(applyRequestFailureAlert, requestPostListFailureAlert) + .asDriver(onErrorJustReturn: .default) - cardClicked - .subscribe(onNext: { [weak self] _ in - guard let self else { return } - - coordinator?.showPostDetail( - postId: postId - ) - }) - .disposed(by: disposeBag) + // MARK: 로딩 + showLoading = Observable + .merge(loadingStartObservables) + .asDriver(onErrorDriveWith: .never()) - starButtonClicked - .subscribe(onNext: { [weak self] _ in - guard let self else { return } - - // 즐겨찾기 버튼눌림 - }) - .disposed(by: disposeBag) + dismissLoading = Observable + .merge(loadingEndObservables) + .delay(.milliseconds(500), scheduler: MainScheduler.instance) + .asDriver(onErrorDriveWith: .never()) + } + + public func showPostDetail(id: String) { + coordinator?.showCenterProfile(centerId: id) } } diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift index a597dc27..31e48ce2 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift @@ -111,6 +111,14 @@ public class WorkerSettingVM: WorkerSettingVMable { ) .asDriver(onErrorJustReturn: ()) + // MARK: 내프로필 보기 + myProfileButtonClicked + .subscribe(onNext: { [weak self] _ in + + self?.coordinator?.showMyProfileScreen() + }) + .disposed(by: disposeBag) + // MARK: 로그아웃 let signOutRequestResult = signOutButtonComfirmed.flatMap({ [settingUseCase] _ in settingUseCase.signoutWorkerAccount() diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift index 9bbf7d77..5d3ef1a2 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift @@ -10,4 +10,7 @@ import Foundation public protocol WorkerSettingScreenCoordinatable: ParentCoordinator { /// 요양보호사 계정을 지우는 작업을 시작합니다. func startRemoveWorkerAccountFlow() + + /// 요양보호사 프로필을 열람합니다. + func showMyProfileScreen() }