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 d52bc0ad..f7fb2469 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift @@ -7,6 +7,7 @@ import UIKit import WorkerFeature +import Entity import BaseFeature import CenterFeature import PresentationCore @@ -65,13 +66,15 @@ class AppliedAndLikedBoardCoordinator: WorkerRecruitmentBoardCoordinatable { } extension AppliedAndLikedBoardCoordinator { - public func showPostDetail(postId: String) { + public func showPostDetail(postType: RecruitmentPostType, postId: String) { let coodinator = PostDetailForWorkerCoodinator( dependency: .init( + postType: postType, postId: postId, parent: self, navigationController: navigationController, - recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self), + workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self) ) ) addChildCoordinator(coodinator) diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerRecruitmentBoardCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerRecruitmentBoardCoordinator.swift index 662d11bb..b4006799 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerRecruitmentBoardCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerRecruitmentBoardCoordinator.swift @@ -9,6 +9,7 @@ import UIKit import WorkerFeature import BaseFeature import CenterFeature +import Entity import PresentationCore import UseCaseInterface @@ -54,13 +55,15 @@ class WorkerRecruitmentBoardCoordinator: WorkerRecruitmentBoardCoordinatable { } extension WorkerRecruitmentBoardCoordinator { - public func showPostDetail(postId: String) { + public func showPostDetail(postType: RecruitmentPostType, postId: String) { let coodinator = PostDetailForWorkerCoodinator( dependency: .init( + postType: postType, postId: postId, parent: self, navigationController: navigationController, - recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self), + workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self) ) ) addChildCoordinator(coodinator) diff --git a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift index 434021fe..6625c5e4 100644 --- a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift +++ b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift @@ -15,11 +15,13 @@ import Moya public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { private var recruitmentPostService: RecruitmentPostService = .init() + private var crawlingPostService: CrawlingPostService = .init() private var applyService: ApplyService = .init() public init(_ store: KeyValueStore? = nil) { if let store { self.recruitmentPostService = RecruitmentPostService(keyValueStore: store) + self.crawlingPostService = CrawlingPostService(keyValueStore: store) self.applyService = ApplyService(keyValueStore: store) } } @@ -91,73 +93,61 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { } // MARK: Worker - public func getPostDetailForWorker(id: String) -> RxSwift.Single { + public func getNativePostDetailForWorker(id: String) -> RxSwift.Single { recruitmentPostService.request( api: .postDetail(id: id, userType: .worker), with: .withToken ) - .map(RecruitmentPostDTO.self) - .map { dto in - dto.toEntity() - } + .mapToEntity(NativeRecruitmentPostDetailDTO.self) + } + + public func getWorknetPostDetailForWorker(id: String) -> RxSwift.Single { + crawlingPostService + .request(api: .getDetail(postId: id), with: .withToken) + .mapToEntity(WorknetRecruitmentPostDetailDTO.self) } - public func getNativePostListForWorker(nextPageId: String?, requestCnt: Int = 10) -> RxSwift.Single { + + public func getNativePostListForWorker(nextPageId: String?, requestCnt: Int = 10) -> RxSwift.Single { 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 { - #if DEBUG - print("앱용 공고 전체조회 에러:", error.localizedDescription) - #endif - } - return .error(error) - }) - .map { dto in - dto.toEntity() - } + .mapToEntity(RecruitmentPostListForWorkerDTO.self) } - public func getFavoritePostListForWorker(nextPageId: String?, requestCnt: Int) -> RxSwift.Single { + public func getNativeFavoritePostListForWorker() -> RxSwift.Single<[RecruitmentPostForWorkerRepresentable]> { recruitmentPostService.request( - api: .getFavoritePostListForWorker(nextPageId: nextPageId, requestCnt: String(requestCnt)), + api: .getNativeFavoritePost, 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() - } + .mapToEntity(FavoriteRecruitmentPostListForWorkerDTO.self) } - public func getAppliedPostListForWorker(nextPageId: String?, requestCnt: Int) -> RxSwift.Single { + public func getWorknetFavoritePostListForWorker() -> RxSwift.Single<[RecruitmentPostForWorkerRepresentable]> { + crawlingPostService.request( + api: .getWorknetFavoritePost, + with: .withToken + ) + .mapToEntity(FavoriteRecruitmentPostListForWorkerDTO.self) + } + + 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() - } + .mapToEntity(RecruitmentPostListForWorkerDTO.self) + } + + public func getWorknetPostListForWorker(nextPageId: String?, requestCnt: Int) -> RxSwift.Single { + crawlingPostService + .request( + api: .getPostList(nextPageId: nextPageId, requestCnt: requestCnt), + with: .withToken + ) + .mapToEntity(RecruitmentPostListForWorkerDTO.self) } public func applyToPost(postId: String, method: ApplyType) -> Single { diff --git a/project/Projects/Data/ConcretesTests/SaveUserInfoDataTests.swift b/project/Projects/Data/ConcretesTests/SaveUserInfoDataTests.swift index 7802a010..1244698d 100644 --- a/project/Projects/Data/ConcretesTests/SaveUserInfoDataTests.swift +++ b/project/Projects/Data/ConcretesTests/SaveUserInfoDataTests.swift @@ -91,7 +91,9 @@ class SaveUserInfoDataTests: XCTestCase { expYear: nil, address: .init(roadAddress: "test", jibunAddress: "test"), introductionText: "test", - specialty: "test" + specialty: "test", + longitude: 0.0, + latitude: 0.0 ) ) diff --git a/project/Projects/Data/DataSource/API/BaseAPI.swift b/project/Projects/Data/DataSource/API/BaseAPI.swift index a3d2fe31..6e0e2cb7 100644 --- a/project/Projects/Data/DataSource/API/BaseAPI.swift +++ b/project/Projects/Data/DataSource/API/BaseAPI.swift @@ -13,6 +13,7 @@ public enum APIType { case auth case users case job_postings + case crawling_job_postings case external(url: String) case applys } @@ -36,6 +37,8 @@ public extension BaseAPI { baseStr += "/users" case .job_postings: baseStr += "/job-postings" + case .crawling_job_postings: + baseStr += "/crawling-job-postings" case .applys: baseStr += "/applys" case .external(let url): diff --git a/project/Projects/Data/DataSource/API/CrawlingPostAPI.swift b/project/Projects/Data/DataSource/API/CrawlingPostAPI.swift new file mode 100644 index 00000000..c5d01545 --- /dev/null +++ b/project/Projects/Data/DataSource/API/CrawlingPostAPI.swift @@ -0,0 +1,78 @@ +// +// CrawlingPostAPI.swift +// DataSource +// +// Created by choijunios on 9/6/24. +// + +import Foundation +import Alamofire +import Moya + +public enum CrawlingPostAPI { + + case getPostList(nextPageId: String?, requestCnt: Int) + case getDetail(postId: String) + case getWorknetFavoritePost +} + +extension CrawlingPostAPI: BaseAPI { + public var apiType: APIType { + .crawling_job_postings + } + + public var path: String { + switch self { + case .getPostList: + "" + case .getDetail(let postId): + "/\(postId)" + case .getWorknetFavoritePost: + "/my/favorites" + } + } + + public var method: Moya.Method { + switch self { + case .getPostList: + .get + case .getDetail: + .get + case .getWorknetFavoritePost: + .get + } + } + + var bodyParameters: Parameters? { + var params: Parameters = [:] + switch self { + case .getPostList(let nextPageId, let requestCnt): + if let nextPageId { + params["next"] = nextPageId + } + params["limit"] = requestCnt + default: + break + } + + return params + } + + var parameterEncoding: ParameterEncoding { + switch self { + case .getPostList: + return URLEncoding.queryString + default: + return JSONEncoding.default + } + } + + public var task: Moya.Task { + switch self { + case .getPostList: + .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) + default: + .requestPlain + } + } +} diff --git a/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift b/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift index b5d6164e..48cf8c5e 100644 --- a/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift +++ b/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift @@ -31,8 +31,10 @@ public enum RcruitmentPostAPI { // Worker case getOnGoingNativePostListForWorker(nextPageId: String?, requestCnt: String) - case getFavoritePostListForWorker(nextPageId: String?, requestCnt: String) case getAppliedPostListForWorker(nextPageId: String?, requestCnt: String) + + // Favorite posts + case getNativeFavoritePost case addFavoritePost(id: String, jobPostingType: RecruitmentPostType) case removeFavoritePost(id: String) } @@ -73,11 +75,12 @@ extension RcruitmentPostAPI: BaseAPI { case .getOnGoingNativePostListForWorker: "" - case .getFavoritePostListForWorker: - "/my/favorites" case .getAppliedPostListForWorker: "/carer/my/applied" + + case .getNativeFavoritePost: + "/my/favorites" case .addFavoritePost(let id, _): "/\(id)/favorites" case .removeFavoritePost(let id): @@ -115,11 +118,11 @@ extension RcruitmentPostAPI: BaseAPI { case .getOnGoingNativePostListForWorker: .get - case .getFavoritePostListForWorker: - .get case .getAppliedPostListForWorker: .get + case .getNativeFavoritePost: + .get case .addFavoritePost: .post case .removeFavoritePost: @@ -135,11 +138,6 @@ 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 @@ -156,7 +154,6 @@ extension RcruitmentPostAPI: BaseAPI { var parameterEncoding: ParameterEncoding { switch self { case .getOnGoingNativePostListForWorker, - .getFavoritePostListForWorker, .getAppliedPostListForWorker: return URLEncoding.queryString default: @@ -167,7 +164,6 @@ extension RcruitmentPostAPI: BaseAPI { public var task: Moya.Task { switch self { case .getOnGoingNativePostListForWorker, - .getFavoritePostListForWorker, .getAppliedPostListForWorker: .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) case .registerPost(let bodyData): diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/NativeRecruitmentPostDetailDTO.swift similarity index 95% rename from project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift rename to project/Projects/Data/DataSource/DTO/RecruitmentPost/NativeRecruitmentPostDetailDTO.swift index da0b1247..b83d80d9 100644 --- a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDetailForWorkerDTO.swift +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/NativeRecruitmentPostDetailDTO.swift @@ -1,5 +1,5 @@ // -// RecruitmentPostDetailForWorkerDTO.swift +// NativeRecruitmentPostDetailDTO.swift // NetworkDataSource // // Created by choijunios on 8/15/24. @@ -8,7 +8,7 @@ import Foundation import Entity -public struct RecruitmentPostDTO: Codable { +public struct NativeRecruitmentPostDetailDTO: EntityRepresentable { public let id: String public let longitude: String @@ -106,8 +106,8 @@ public struct RecruitmentPostDTO: Codable { ) let jobLocation: LocationInformation = .init( - longitude: Double(longitude)!, - latitude: Double(latitude)! + longitude: Double(longitude) ?? 0.0, + latitude: Double(latitude) ?? 0.0 ) // MARK: Apply date diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift index 87211adf..b34ef0c7 100644 --- a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecuritmentPostListForWorkerDTO.swift @@ -8,9 +8,14 @@ import Foundation import Entity -public struct RecruitmentPostListForWorkerDTO: Codable { +public protocol EntityRepresentable: Codable { + associatedtype Entity + func toEntity() -> Entity +} + +public struct RecruitmentPostListForWorkerDTO: EntityRepresentable where T.Entity: RecruitmentPostForWorkerRepresentable { - public let items: [RecruitmentPostForWorkerDTO] + public let items: [T] public let next: String? public let total: Int @@ -24,7 +29,63 @@ public struct RecruitmentPostListForWorkerDTO: Codable { } } -public struct RecruitmentPostForWorkerDTO: Codable { +public struct FavoriteRecruitmentPostListForWorkerDTO: EntityRepresentable where T.Entity: RecruitmentPostForWorkerRepresentable { + + public let favoriteJobPostings: [T] + + public func toEntity() -> [RecruitmentPostForWorkerRepresentable] { + + favoriteJobPostings.map { dto in + dto.toEntity() + } + } +} + +// MARK: Worknet post의 카드 정보 +public struct WorkNetRecruitmentPostForWorkerDTO: EntityRepresentable { + + public let id: String + public let title: String + public let distance: Int + public let workingTime: String + public let workingSchedule: String + public let payInfo: String + public let applyDeadline: String + public let isFavorite: Bool + public let jobPostingType: RecruitmentPostType + public let createdAt: String? + + public func toEntity() -> WorknetRecruitmentPostVO { + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + let deadlineDate = dateFormatter.date(from: self.applyDeadline)! + + let iso8601Formatter = ISO8601DateFormatter() + var createdDate: Date? + if let createdAt { + createdDate = iso8601Formatter.date(from: createdAt) + } + + return .init( + id: id, + title: title, + distance: distance, + workingTime: workingTime, + workingSchedule: workingSchedule, + payInfo: payInfo, + applyDeadline: deadlineDate, + isFavorite: isFavorite, + postType: jobPostingType, + beFavoritedTime: createdDate + ) + } +} + +// MARK: Native Post의 카드 정보입니다. +public struct NativeRecruitmentPostForWorkerDTO: EntityRepresentable { + public let isExperiencePreferred: Bool public let id: String public let weekdays: [String] @@ -42,6 +103,8 @@ public struct RecruitmentPostForWorkerDTO: Codable { public let distance: Int public let applyTime: String? public let isFavorite: Bool + public let jobPostingType: RecruitmentPostType + public let createdAt: String? public func toEntity() -> NativeRecruitmentPostForWorkerVO { @@ -55,6 +118,12 @@ public struct RecruitmentPostForWorkerDTO: Codable { let deadlineDate = self.applyDeadline != nil ? dateFormatter.date(from: self.applyDeadline!) : nil let applyDate = self.applyTime != nil ? dateFormatter.date(from: self.applyDeadline!) : nil + let iso8601Formatter = ISO8601DateFormatter() + var createdDate: Date? + if let createdAt { + createdDate = iso8601Formatter.date(from: createdAt) + } + return .init( postId: id, workDays: workDayList, @@ -72,7 +141,10 @@ public struct RecruitmentPostForWorkerDTO: Codable { payAmount: String(payAmount), distanceFromWorkPlace: distance, applyTime: applyDate, - isFavorite: isFavorite + isFavorite: isFavorite, + postType: jobPostingType, + beFavoritedTime: createdDate ) } } + diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/WorknetRecruitmentPostDetailDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/WorknetRecruitmentPostDetailDTO.swift new file mode 100644 index 00000000..26aea2d0 --- /dev/null +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/WorknetRecruitmentPostDetailDTO.swift @@ -0,0 +1,65 @@ +// +// WorknetRecruitmentPostDetailDTO.swift +// DataSource +// +// Created by choijunios on 9/6/24. +// + +import Foundation +import Entity + +public struct WorknetRecruitmentPostDetailDTO: EntityRepresentable { + + let id: String + let title: String + let content: String + let clientAddress: String + let longitude: String + let latitude: String + let distance: Int + let createdAt: String + let payInfo: String + let workingTime: String + let workingSchedule: String + let applyDeadline: String + let recruitmentProcess: String + let applyMethod: String + let requiredDocumentation: String + let centerName: String + let centerAddress: String + let jobPostingUrl: String + let jobPostingType: RecruitmentPostType + let isFavorite: Bool + + public func toEntity() -> WorknetRecruitmentPostDetailVO { + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + let createdAtDate = dateFormatter.date(from: createdAt) ?? Date() + let applyDeadlineDate = dateFormatter.date(from: applyDeadline) ?? Date() + + return .init( + id: id, + title: title, + content: content, + clientAddress: clientAddress, + longitude: longitude, + latitude: latitude, + distance: distance, + createdAt: createdAtDate, + payInfo: payInfo, + workingTime: workingTime, + workingSchedule: workingSchedule, + applyDeadline: applyDeadlineDate, + recruitmentProcess: recruitmentProcess, + applyMethod: applyMethod, + requiredDocumentation: requiredDocumentation, + centerName: centerName, + centerAddress: centerAddress, + jobPostingUrl: jobPostingUrl, + jobPostingType: jobPostingType, + isFavorite: isFavorite + ) + } +} diff --git a/project/Projects/Data/DataSource/DTO/UserInfo/WorkerProfileDTO.swift b/project/Projects/Data/DataSource/DTO/UserInfo/WorkerProfileDTO.swift index 8072ab90..2669b62f 100644 --- a/project/Projects/Data/DataSource/DTO/UserInfo/WorkerProfileDTO.swift +++ b/project/Projects/Data/DataSource/DTO/UserInfo/WorkerProfileDTO.swift @@ -20,7 +20,8 @@ public struct CarerProfileDTO: Codable { let speciality: String? let profileImageUrl: String? let jobSearchStatus: String - + let longitude: String + let latitude: String public func toVO() -> WorkerProfileVO { @@ -37,7 +38,9 @@ public struct CarerProfileDTO: Codable { jibunAddress: lotNumberAddress ), introductionText: introduce ?? "", - specialty: speciality ?? "" + specialty: speciality ?? "", + longitude: Double(longitude)!, + latitude: Double(latitude)! ) } } diff --git a/project/Projects/Data/DataSource/Service/asd.swift b/project/Projects/Data/DataSource/Service/asd.swift new file mode 100644 index 00000000..325b7f12 --- /dev/null +++ b/project/Projects/Data/DataSource/Service/asd.swift @@ -0,0 +1,17 @@ +// +// CrawlingPostService.swift +// DataSource +// +// Created by choijunios on 9/6/24. +// + +import Foundation + +public class CrawlingPostService: BaseNetworkService { + + public init() { } + + public override init(keyValueStore: KeyValueStore) { + super.init(keyValueStore: keyValueStore) + } +} diff --git a/project/Projects/Data/DataSource/Util/Extension/Single+Extension.swift b/project/Projects/Data/DataSource/Util/Extension/Single+Extension.swift index 98393f93..252f8d9b 100644 --- a/project/Projects/Data/DataSource/Util/Extension/Single+Extension.swift +++ b/project/Projects/Data/DataSource/Util/Extension/Single+Extension.swift @@ -12,4 +12,20 @@ public extension PrimitiveSequence where Trait == SingleTrait, Element == Respon func mapToVoid() -> Single { flatMap { _ in .just(()) } } + + func mapToEntity(_ type: T.Type) -> Single { + map(T.self) + .catch({ error in + if let moyaError = error as? MoyaError, case .objectMapping(let error, _) = moyaError { + #if DEBUG + print("[디코딩에러] \(String(describing: T.self))") + #endif + } + return .error(error) + }) + .map { dto in + dto.toEntity() + } + + } } diff --git a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift index f02a905f..58969877 100644 --- a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift @@ -53,8 +53,12 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { convert(task: repository.getPostDetailForCenter(id: id)) } - public func getPostDetailForWorker(id: String) -> RxSwift.Single> { - convert(task: repository.getPostDetailForWorker(id: id)) + public func getNativePostDetailForWorker(id: String) -> RxSwift.Single> { + convert(task: repository.getNativePostDetailForWorker(id: id)) + } + + public func getWorknetPostDetailForWorker(id: String) -> RxSwift.Single> { + convert(task: repository.getWorknetPostDetailForWorker(id: id)) } public func getOngoingPosts() -> RxSwift.Single> { @@ -111,38 +115,28 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { requestCnt: postCount ) case .thirdParty: - // TODO: ‼️ ‼️워크넷 가져오기 미구현 - fatalError() + stream = repository.getWorknetPostListForWorker( + nextPageId: nextPageId, + requestCnt: postCount + ) } } return convert(task: stream) } - public func getFavoritePostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> RxSwift.Single> { + public func getFavoritePostListForWorker() -> RxSwift.Single> { - let stream: Single! + let nativeList = repository.getNativeFavoritePostListForWorker() + let worknetList = repository.getWorknetFavoritePostListForWorker() - 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() + let task = Single + .zip(nativeList, worknetList) + .map { (native, worknet) in + native + worknet } - } - return convert(task: stream) + return convert(task: task) } public func getAppliedPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> RxSwift.Single> { @@ -163,9 +157,7 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { requestCnt: postCount ) case .thirdParty: - - // TODO: ‼️ ‼️워크넷 가져오기 미구현 - return .just(.failure(.notImplemented)) + return .just(.failure(.undefinedError)) } } diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift index e7d44276..35ac8da8 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift @@ -28,7 +28,11 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { return .just(.success(cachedProfile)) } - return convert(task: userProfileRepository.getCenterProfile(mode: mode)) + return getFreshProfile(mode: mode) + } + + public func getFreshProfile(mode: Entity.ProfileMode) -> RxSwift.Single> { + convert(task: userProfileRepository.getCenterProfile(mode: mode)) } public func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> { diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift index 097b7474..7d431d1c 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift @@ -28,7 +28,11 @@ public class DefaultWorkerProfileUseCase: WorkerProfileUseCase { return .just(.success(cachedProfile)) } - return convert(task: userProfileRepository.getWorkerProfile(mode: mode)) + return getFreshProfile(mode: mode) + } + + public func getFreshProfile(mode: Entity.ProfileMode) -> RxSwift.Single> { + convert(task: userProfileRepository.getWorkerProfile(mode: mode)) } public func updateProfile(stateObject: WorkerProfileStateObject, imageInfo: ImageUploadInfo?) -> Single> { diff --git a/project/Projects/Domain/Entity/State/Util/RecruitmentPostType.swift b/project/Projects/Domain/Entity/State/Util/RecruitmentPostType.swift index ae4a5737..aedecc6d 100644 --- a/project/Projects/Domain/Entity/State/Util/RecruitmentPostType.swift +++ b/project/Projects/Domain/Entity/State/Util/RecruitmentPostType.swift @@ -7,9 +7,9 @@ import Foundation -public enum RecruitmentPostType { - case native - case workNet +public enum RecruitmentPostType: String, Codable { + case native="CAREMEET" + case workNet="WORKNET" public var upscaleEngWord: String { switch self { diff --git a/project/Projects/Domain/Entity/VO/AlertContentVO.swift b/project/Projects/Domain/Entity/VO/AlertContentVO.swift index c2eb64d1..ee5d4ee4 100644 --- a/project/Projects/Domain/Entity/VO/AlertContentVO.swift +++ b/project/Projects/Domain/Entity/VO/AlertContentVO.swift @@ -11,10 +11,12 @@ public struct DefaultAlertContentVO { public let title: String public let message: String + public let onDismiss: (() -> ())? - public init(title: String, message: String) { + public init(title: String, message: String, onDismiss: (() -> ())? = nil) { self.title = title self.message = message + self.onDismiss = onDismiss } public static let `default` = DefaultAlertContentVO( diff --git a/project/Projects/Domain/Entity/VO/Post/Detail/WorknetRecruitmentPostDetailVO.swift b/project/Projects/Domain/Entity/VO/Post/Detail/WorknetRecruitmentPostDetailVO.swift new file mode 100644 index 00000000..326403df --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Post/Detail/WorknetRecruitmentPostDetailVO.swift @@ -0,0 +1,76 @@ +// +// WorknetRecruitmentPostDetailVO.swift +// Entity +// +// Created by choijunios on 9/6/24. +// + +import Foundation + +public struct WorknetRecruitmentPostDetailVO: Decodable { + + public let id: String + public let title: String + public let content: String + public let clientAddress: String + public let longitude: String + public let latitude: String + public let distance: Int + public let createdAt: Date + public let payInfo: String + public let workingTime: String + public let workingSchedule: String + public let applyDeadline: Date + public let recruitmentProcess: String + public let applyMethod: String + public let requiredDocumentation: String + public let centerName: String + public let centerAddress: String + public let jobPostingUrl: String + public let jobPostingType: RecruitmentPostType + public let isFavorite: Bool + + public init( + id: String, + title: String, + content: String, + clientAddress: String, + longitude: String, + latitude: String, + distance: Int, + createdAt: Date, + payInfo: String, + workingTime: String, + workingSchedule: String, + applyDeadline: Date, + recruitmentProcess: String, + applyMethod: String, + requiredDocumentation: String, + centerName: String, + centerAddress: String, + jobPostingUrl: String, + jobPostingType: RecruitmentPostType, + isFavorite: Bool + ) { + self.id = id + self.title = title + self.content = content + self.clientAddress = clientAddress + self.longitude = longitude + self.latitude = latitude + self.distance = distance + self.createdAt = createdAt + self.payInfo = payInfo + self.workingTime = workingTime + self.workingSchedule = workingSchedule + self.applyDeadline = applyDeadline + self.recruitmentProcess = recruitmentProcess + self.applyMethod = applyMethod + self.requiredDocumentation = requiredDocumentation + self.centerName = centerName + self.centerAddress = centerAddress + self.jobPostingUrl = jobPostingUrl + self.jobPostingType = jobPostingType + self.isFavorite = isFavorite + } +} diff --git a/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift b/project/Projects/Domain/Entity/VO/Post/List/NativeRecruitmentPostForWorkerVO.swift similarity index 66% rename from project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift rename to project/Projects/Domain/Entity/VO/Post/List/NativeRecruitmentPostForWorkerVO.swift index 9d366113..b771c191 100644 --- a/project/Projects/Domain/Entity/VO/Post/RecruitmentPostListForWorkerVO.swift +++ b/project/Projects/Domain/Entity/VO/Post/List/NativeRecruitmentPostForWorkerVO.swift @@ -1,5 +1,5 @@ // -// RecruitmentPostListForWorkerVO.swift +// NativeRecruitmentPostForWorkerVO.swift // Entity // // Created by choijunios on 8/16/24. @@ -7,20 +7,12 @@ import Foundation -public struct RecruitmentPostListForWorkerVO { - - public let posts: [NativeRecruitmentPostForWorkerVO] - public let nextPageId: String? - public let fetchedPostCount: Int +public struct NativeRecruitmentPostForWorkerVO: RecruitmentPostForWorkerRepresentable { + + // protocol required + public var postType: RecruitmentPostType + public var beFavoritedTime: Date? - public init(posts: [NativeRecruitmentPostForWorkerVO], nextPageId: String?, fetchedPostCount: Int) { - self.posts = posts - self.nextPageId = nextPageId - self.fetchedPostCount = fetchedPostCount - } -} - -public struct NativeRecruitmentPostForWorkerVO { public let postId: String public let workDays: [WorkDay] @@ -44,7 +36,28 @@ public struct NativeRecruitmentPostForWorkerVO { 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: Int, applyTime: Date?, 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: Int, + applyTime: Date?, + isFavorite: Bool, + postType: RecruitmentPostType, + beFavoritedTime: Date? + ) { self.postId = postId self.workDays = workDays self.startTime = startTime @@ -62,6 +75,8 @@ public struct NativeRecruitmentPostForWorkerVO { self.distanceFromWorkPlace = distanceFromWorkPlace self.applyTime = applyTime self.isFavorite = isFavorite + self.postType = postType + self.beFavoritedTime = beFavoritedTime } public static let mock = NativeRecruitmentPostForWorkerVO( @@ -81,6 +96,8 @@ public struct NativeRecruitmentPostForWorkerVO { payAmount: "15000", distanceFromWorkPlace: 2500, applyTime: Date(), - isFavorite: true + isFavorite: true, + postType: .native, + beFavoritedTime: Calendar.current.date(byAdding: .day, value: 7, to: Date()) ) } diff --git a/project/Projects/Domain/Entity/VO/Post/List/RecruitmentPostListForWorkerVO.swift b/project/Projects/Domain/Entity/VO/Post/List/RecruitmentPostListForWorkerVO.swift new file mode 100644 index 00000000..87186311 --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Post/List/RecruitmentPostListForWorkerVO.swift @@ -0,0 +1,25 @@ +// +// RecruitmentPostListForWorkerVO.swift +// Entity +// +// Created by choijunios on 9/6/24. +// + +import Foundation + +public struct RecruitmentPostListForWorkerVO { + + public let posts: [RecruitmentPostForWorkerRepresentable] + public let nextPageId: String? + public let fetchedPostCount: Int + + public init(posts: [RecruitmentPostForWorkerRepresentable], nextPageId: String?, fetchedPostCount: Int) { + self.posts = posts + self.nextPageId = nextPageId + self.fetchedPostCount = fetchedPostCount + } +} +public protocol RecruitmentPostForWorkerRepresentable { + var postType: RecruitmentPostType { get } + var beFavoritedTime: Date? { get } +} diff --git a/project/Projects/Domain/Entity/VO/Post/List/WorknetRecruitmentPostForWorkerVO.swift b/project/Projects/Domain/Entity/VO/Post/List/WorknetRecruitmentPostForWorkerVO.swift new file mode 100644 index 00000000..2a69386f --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Post/List/WorknetRecruitmentPostForWorkerVO.swift @@ -0,0 +1,48 @@ +// +// WorknetRecruitmentPostVO.swift +// Entity +// +// Created by choijunios on 9/6/24. +// + +import Foundation + +public struct WorknetRecruitmentPostVO: RecruitmentPostForWorkerRepresentable { + + // protocol required + public var postType: RecruitmentPostType + public var beFavoritedTime: Date? + + public let id: String + public let title: String + public let distance: Int + public let workingTime: String + public let workingSchedule: String + public let payInfo: String + public let applyDeadline: Date + public let isFavorite: Bool + + public init( + id: String, + title: String, + distance: Int, + workingTime: String, + workingSchedule: String, + payInfo: String, + applyDeadline: Date, + isFavorite: Bool, + postType: RecruitmentPostType, + beFavoritedTime: Date? = nil + ) { + self.id = id + self.title = title + self.distance = distance + self.workingTime = workingTime + self.workingSchedule = workingSchedule + self.payInfo = payInfo + self.applyDeadline = applyDeadline + self.isFavorite = isFavorite + self.postType = postType + self.beFavoritedTime = beFavoritedTime + } +} diff --git a/project/Projects/Domain/Entity/VO/UserInfo/WorkerProfileVO.swift b/project/Projects/Domain/Entity/VO/UserInfo/WorkerProfileVO.swift index f1947225..51c3ccee 100644 --- a/project/Projects/Domain/Entity/VO/UserInfo/WorkerProfileVO.swift +++ b/project/Projects/Domain/Entity/VO/UserInfo/WorkerProfileVO.swift @@ -10,8 +10,8 @@ import Foundation public struct WorkerProfileVO: Codable { public let profileImageURL: String? - - + public let longitude: Double + public let latitude: Double public let nameText: String public let phoneNumber: String public let isLookingForJob: Bool @@ -32,7 +32,9 @@ public struct WorkerProfileVO: Codable { expYear: Int?, address: AddressInformation, introductionText: String, - specialty: String + specialty: String, + longitude: Double, + latitude: Double ) { self.profileImageURL = profileImageURL self.nameText = nameText @@ -44,6 +46,8 @@ public struct WorkerProfileVO: Codable { self.address = address self.introductionText = introductionText self.specialty = specialty + self.latitude = latitude + self.longitude = longitude } } @@ -61,6 +65,8 @@ public extension WorkerProfileVO { jibunAddress: "" ), introductionText: "", - specialty: "" + specialty: "", + longitude: 0.0, + latitude: 0.0 ) } diff --git a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift index e45eb8a9..e920fd7c 100644 --- a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift @@ -41,18 +41,31 @@ public protocol RecruitmentPostRepository: RepositoryBase { func removePost(id: String) -> Single // MARK: Worker - /// 요양보호사 공고의 상세정보를 조회합니다. - func getPostDetailForWorker(id: String) -> Single + /// 요양보호사 앱내 공고의 상세정보를 조회합니다. + func getNativePostDetailForWorker(id: String) -> Single + + /// 요양보호사 워크넷 공고의 상세정보를 조회합니다. + func getWorknetPostDetailForWorker(id: String) -> Single + + // MARK: Native post /// 요양보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. func getNativePostListForWorker(nextPageId: String?, requestCnt: Int) -> Single - /// 요양보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. - func getFavoritePostListForWorker(nextPageId: String?, requestCnt: Int) -> Single + /// 요양보호사가 즐겨찾는 케어밋 자체 공고정보를 가져옵니다. + func getNativeFavoritePostListForWorker() -> Single<[RecruitmentPostForWorkerRepresentable]> + + /// 요양보호사가 즐겨찾는 워크넷 공고정보를 가져옵니다. + func getWorknetFavoritePostListForWorker() -> Single<[RecruitmentPostForWorkerRepresentable]> /// 요양보호사가 확인하는 케어밋 자체 공고정보를 가져옵니다. func getAppliedPostListForWorker(nextPageId: String?, requestCnt: Int) -> Single + // MARK: Worknet Post + + /// 요양보호사가 확인하는 워크넷 공고정보를 가져옵니다. + func getWorknetPostListForWorker(nextPageId: String?, requestCnt: Int) -> Single + /// 요양보호사가 인앱 공고에 지원합니다. func applyToPost(postId: String, method: ApplyType) -> Single diff --git a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift index 837fb648..a1689a7a 100644 --- a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift @@ -47,13 +47,16 @@ public protocol RecruitmentPostUseCase: UseCaseBase { /// - 공고상세정보(센터와 달리 고객 이름 배제) /// - 근무지 위치(위경도) /// - 센터정보(센터 id, 이름, 도로명 주소) - func getPostDetailForWorker(id: String) -> Single> + func getNativePostDetailForWorker(id: String) -> Single> + + /// 워크넷 공고 상세정보를 반환합니다. + func getWorknetPostDetailForWorker(id: String) -> Single> /// 요양보호사가 메인화면에 사용할 공고리스트를 호출합니다. func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> /// 요양보호사가 즐겨찾기한 공고리스트를 호출합니다. - func getFavoritePostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> + func getFavoritePostListForWorker() -> Single> /// 요양보호사가 지원한 공고리스트를 호출합니다. func getAppliedPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift index 42cbf4c2..1193d9bc 100644 --- a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift @@ -20,8 +20,12 @@ public protocol CenterProfileUseCase: UseCaseBase { /// 1. 나의 센터/다른 센터 프로필 정보 조회 /// 6. 특정 센터의 프로필 불러오기 + /// 캐시된 데이터가 있을 경우 해당 데이터를 가져옵니다. func getProfile(mode: ProfileMode) -> Single> + /// 캐쉬되지 않은 정보를 가져옵니다. + func getFreshProfile(mode: ProfileMode) -> Single> + /// 2. 센터 프로필 정보 업데이트(전화번호, 센터소개글) /// 3. 센터 프로필 정보 업데이트(이미지, pre-signed-url) /// 4. 센터 프로필 정보 업데이트(이미지, pre-signed-url-callback) diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift index 86c4ee25..f2669f37 100644 --- a/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift @@ -21,6 +21,8 @@ public protocol WorkerProfileUseCase: UseCaseBase { /// 5. 특정 요양보호사의 프로필 불러오기 func getProfile(mode: ProfileMode) -> Single> + func getFreshProfile(mode: ProfileMode) -> Single> + /// 2. 나의(요보) 프로필 정보 업데이트(텍스트 데이터) /// 3. 나의(요보) 프로필 정보 업데이트(이미지, pre-signed-url) diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardCell.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardCell.swift index 6a9dcaae..03f5f4f8 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardCell.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardCell.swift @@ -42,10 +42,9 @@ public class CenterEmployCardCell: UITableViewCell { // Row5 lazy var buttonStack: VStack = { - let belowButtonStack = HStack([editPostButton, terminatePostButton,], spacing: 4) let stack = VStack([ checkApplicantsButton, - HStack([belowButtonStack, Spacer()]) + HStack([editPostButton, terminatePostButton, Spacer()], spacing: 4, distribution: .fill) ], spacing: 8, alignment: .fill) return stack }() @@ -88,38 +87,48 @@ public class CenterEmployCardCell: UITableViewCell { disposables = nil } - public override func layoutSubviews() { - super.layoutSubviews() - - contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 8, right: 20)) - } - func setAppearance() { - contentView.backgroundColor = DSColor.gray0.color - contentView.layer.setGrayBorder() + contentView.backgroundColor = .clear } func setLayout() { - contentView.layoutMargins = .init(top: 16, left: 16, bottom: 16, right: 16) + let cellView = UIView() + cellView.layoutMargins = .init(top: 16, left: 16, bottom: 16, right: 16) + cellView.backgroundColor = DSColor.gray0.color + cellView.layer.setGrayBorder() let contentStack = VStack([ cardView, buttonStack ], spacing: 12, alignment: .fill) - + [ contentStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + cellView.addSubview($0) + } + + NSLayoutConstraint.activate([ + contentStack.topAnchor.constraint(equalTo: cellView.layoutMarginsGuide.topAnchor), + contentStack.leftAnchor.constraint(equalTo: cellView.layoutMarginsGuide.leftAnchor), + contentStack.rightAnchor.constraint(equalTo: cellView.layoutMarginsGuide.rightAnchor), + contentStack.bottomAnchor.constraint(equalTo: cellView.layoutMarginsGuide.bottomAnchor), + ]) + + [ + cellView ].forEach { $0.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview($0) } NSLayoutConstraint.activate([ - contentStack.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), - contentStack.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), - contentStack.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), - contentStack.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), + cellView.topAnchor.constraint(equalTo: contentView.topAnchor), + cellView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + cellView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + cellView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), ]) } diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCard.swift similarity index 65% rename from project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift rename to project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCard.swift index a55d154a..fdad1bd7 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCard.swift @@ -1,8 +1,8 @@ // -// WorkerNativeEmployCardRO.swift +// WorkerNativeEmployCard.swift // DSKit // -// Created by choijunios on 7/19/24. +// Created by choijunios on 9/6/24. // import UIKit @@ -10,125 +10,7 @@ import RxSwift import RxCocoa import Entity -public class WorkerNativeEmployCardRO { - - let showBiginnerTag: Bool - let titleText: String - let timeDurationForWalkingText: String - let targetInfoText: String - let workDaysText: String - let workTimeText: String - let payText: String - let isFavorite: Bool - - init( - showBiginnerTag: Bool, - titleText: String, - timeDurationForWalkingText: String, - targetInfoText: String, - workDaysText: String, - workTimeText: String, - payText: String, - isFavorite: Bool - ) { - self.showBiginnerTag = showBiginnerTag - self.titleText = titleText - self.timeDurationForWalkingText = timeDurationForWalkingText - self.targetInfoText = targetInfoText - self.workDaysText = workDaysText - self.workTimeText = workTimeText - self.payText = payText - self.isFavorite = isFavorite - } - - public static func create(vo: WorkerNativeEmployCardVO) -> WorkerNativeEmployCardRO { - -// var dayLeftTagText: String? = nil -// var showDayLeftTag: Bool = false -// -// if (0...14).contains(vo.dayLeft) { -// showDayLeftTag = true -// dayLeftTagText = vo.dayLeft == 0 ? "D-Day" : "D-\(vo.dayLeft)" -// } - - let targetInfoText = "\(vo.careGrade.textForCellBtn)등급 \(vo.targetAge)세 \(vo.targetGender.twoLetterKoreanWord)" - - let workDaysText = vo.days.sorted(by: { d1, d2 in - d1.rawValue < d2.rawValue - }).map({ $0.korOneLetterText }).joined(separator: ",") - - let workTimeText = "\(vo.startTime) - \(vo.endTime)" - - var formedPayAmountText = "" - for (index, char) in vo.paymentAmount.reversed().enumerated() { - if (index % 3) == 0, index != 0 { - formedPayAmountText = "," + formedPayAmountText - } - formedPayAmountText = String(char) + formedPayAmountText - } - - let payText = "\(vo.paymentType.korLetterText) \(formedPayAmountText) 원" - - var splittedAddress = vo.title.split(separator: " ") - - if splittedAddress.count >= 3 { - splittedAddress = Array(splittedAddress[0..<3]) - } - let addressTitle = splittedAddress.joined(separator: " ") - - // distance는 미터단위입니다. - let durationText = Self.timeForDistance(meter: vo.distanceFromWorkPlace) - - return .init( - showBiginnerTag: vo.isBeginnerPossible, - titleText: addressTitle, - timeDurationForWalkingText: durationText, - targetInfoText: targetInfoText, - workDaysText: workDaysText, - workTimeText: workTimeText, - payText: payText, - isFavorite: vo.isFavorite - ) - } - - public static let `mock`: WorkerNativeEmployCardRO = .init( - showBiginnerTag: true, - titleText: "사울시 강남동", - timeDurationForWalkingText: "도보 15분 ~ 20분", - targetInfoText: "1등급 54세 여성", - workDaysText: "", - workTimeText: "월, 화, 수", - payText: "시급 5000원", - isFavorite: true - ) - - static func timeForDistance(meter: Int) -> String { - switch meter { - case 0..<200: - return "도보 5분 이내" - case 200..<400: - return "도보 5 ~ 10분" - case 400..<700: - return "도보 10 ~ 15분" - case 700..<1000: - return "도보 15 ~ 20분" - case 1000..<1250: - return "도보 20 ~ 25분" - case 1250..<1500: - return "도보 25 ~ 30분" - case 1500..<1750: - return "도보 30 ~ 35분" - case 1750..<2000: - return "도보 35 ~ 40분" - default: - return "도보 40분 ~" - } - } - -} - - -public class WorkerEmployCard: UIView { +public class WorkerNativeEmployCard: UIView { // View public let starButton: IconWithColorStateButton = { @@ -195,7 +77,9 @@ public class WorkerEmployCard: UIView { public required init?(coder: NSCoder) { fatalError() } - func setAppearance() { } + func setAppearance() { + self.backgroundColor = DSColor.gray0.color + } func setLayout() { @@ -308,7 +192,6 @@ public class WorkerEmployCard: UIView { alignment: .leading ) ] - let mainStack = VStack( stackList, diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerNativeEmployCardCell.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCardCell.swift similarity index 83% rename from project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerNativeEmployCardCell.swift rename to project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCardCell.swift index bcf58d48..f2386f57 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerNativeEmployCardCell.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCardCell.swift @@ -15,35 +15,35 @@ public enum PostAppliedState { case notApplied } -public protocol WorkerNativeEmployCardViewModelable: AnyObject { - - /// '지원하기' 버튼이 눌렸을 때, 공고 id를 전달합니다. - var applyButtonClicked: PublishRelay<(postId: String, postTitle: String)> { get } +public protocol WorkerEmployCardViewModelable: AnyObject { /// 공고상세보기 - func showPostDetail(id: String) + func showPostDetail(postType: RecruitmentPostType, id: String) /// 즐겨찾기 버튼 클릭 func setPostFavoriteState(isFavoriteRequest: Bool, postId: String, postType: RecruitmentPostType) -> Single } +public protocol AppliableWorkerEmployCardVMable: WorkerEmployCardViewModelable { + /// '지원하기' 버튼이 눌렸을 때, 공고 id를 전달합니다. + var applyButtonClicked: PublishRelay<(postId: String, postTitle: String)> { get } +} + public class WorkerNativeEmployCardCell: UITableViewCell { public static let identifier = String(describing: WorkerNativeEmployCardCell.self) - - var viewModel: WorkerNativeEmployCardViewModelable? private var disposables: [Disposable?]? - public override func layoutSubviews() { - super.layoutSubviews() - - contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 8, right: 20)) - } - // View - let tappableArea: TappableUIView = .init() - let cardView = WorkerEmployCard() + let tappableArea: TappableUIView = { + let view = TappableUIView() + view.layer.borderWidth = 1 + view.layer.cornerRadius = 12 + view.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor + return view + }() + let cardView = WorkerNativeEmployCard() let applyButton: IdlePrimaryCardButton = { let btn = IdlePrimaryCardButton(level: .large) btn.label.textString = "지원하기" @@ -58,16 +58,12 @@ public class WorkerNativeEmployCardCell: UITableViewCell { public required init?(coder: NSCoder) { fatalError() } public override func prepareForReuse() { - viewModel = nil - disposables?.forEach { $0?.dispose() } disposables = nil } func setAppearance() { - contentView.layer.borderWidth = 1 - contentView.layer.cornerRadius = 12 - contentView.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor + contentView.backgroundColor = .clear } func setLayout() { @@ -95,13 +91,15 @@ public class WorkerNativeEmployCardCell: UITableViewCell { mainStack.bottomAnchor.constraint(equalTo: tappableArea.layoutMarginsGuide.bottomAnchor), tappableArea.topAnchor.constraint(equalTo: contentView.topAnchor), - tappableArea.leftAnchor.constraint(equalTo: contentView.leftAnchor), - tappableArea.rightAnchor.constraint(equalTo: contentView.rightAnchor), - tappableArea.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + tappableArea.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + tappableArea.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + tappableArea.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), ]) } - public func bind(postId: String, vo: WorkerNativeEmployCardVO, viewModel: WorkerNativeEmployCardViewModelable) { + public func bind(postId: String, vo: WorkerNativeEmployCardVO, viewModel: WorkerEmployCardViewModelable) { + + guard let appliableVM = viewModel as? AppliableWorkerEmployCardVMable else { return } // 지원 여부 if let appliedDate = vo.applyDate { @@ -139,12 +137,12 @@ public class WorkerNativeEmployCardCell: UITableViewCell { tappableArea .rx.tap .subscribe(onNext: { [weak viewModel] _ in - viewModel?.showPostDetail(id: postId) + viewModel?.showPostDetail(postType: .native, id: postId) }), applyButton.rx.tap .map({ _ in (postId, vo.title) }) - .bind(to: viewModel.applyButtonClicked), + .bind(to: appliableVM.applyButtonClicked), favoriteRequestResult .subscribe(onNext: { [starButton] isSuccess in diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCardRO.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCardRO.swift new file mode 100644 index 00000000..a0f6bad9 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/NativeCard/WorkerNativeEmployCardRO.swift @@ -0,0 +1,128 @@ +// +// WorkerNativeEmployCardRO.swift +// DSKit +// +// Created by choijunios on 7/19/24. +// + +import UIKit +import RxSwift +import RxCocoa +import Entity + +// MARK: Render object +public class WorkerNativeEmployCardRO { + + let showBiginnerTag: Bool + let titleText: String + let timeDurationForWalkingText: String + let targetInfoText: String + let workDaysText: String + let workTimeText: String + let payText: String + let isFavorite: Bool + + init( + showBiginnerTag: Bool, + titleText: String, + timeDurationForWalkingText: String, + targetInfoText: String, + workDaysText: String, + workTimeText: String, + payText: String, + isFavorite: Bool + ) { + self.showBiginnerTag = showBiginnerTag + self.titleText = titleText + self.timeDurationForWalkingText = timeDurationForWalkingText + self.targetInfoText = targetInfoText + self.workDaysText = workDaysText + self.workTimeText = workTimeText + self.payText = payText + self.isFavorite = isFavorite + } + + public static func create(vo: WorkerNativeEmployCardVO) -> WorkerNativeEmployCardRO { + +// var dayLeftTagText: String? = nil +// var showDayLeftTag: Bool = false +// +// if (0...14).contains(vo.dayLeft) { +// showDayLeftTag = true +// dayLeftTagText = vo.dayLeft == 0 ? "D-Day" : "D-\(vo.dayLeft)" +// } + + let targetInfoText = "\(vo.careGrade.textForCellBtn)등급 \(vo.targetAge)세 \(vo.targetGender.twoLetterKoreanWord)" + + let workDaysText = vo.days.sorted(by: { d1, d2 in + d1.rawValue < d2.rawValue + }).map({ $0.korOneLetterText }).joined(separator: ",") + + let workTimeText = "\(vo.startTime) - \(vo.endTime)" + + var formedPayAmountText = "" + for (index, char) in vo.paymentAmount.reversed().enumerated() { + if (index % 3) == 0, index != 0 { + formedPayAmountText = "," + formedPayAmountText + } + formedPayAmountText = String(char) + formedPayAmountText + } + + let payText = "\(vo.paymentType.korLetterText) \(formedPayAmountText) 원" + + var splittedAddress = vo.title.split(separator: " ") + + if splittedAddress.count >= 3 { + splittedAddress = Array(splittedAddress[0..<3]) + } + let addressTitle = splittedAddress.joined(separator: " ") + + // distance는 미터단위입니다. + let durationText = Self.timeForDistance(meter: vo.distanceFromWorkPlace) + + return .init( + showBiginnerTag: vo.isBeginnerPossible, + titleText: addressTitle, + timeDurationForWalkingText: durationText, + targetInfoText: targetInfoText, + workDaysText: workDaysText, + workTimeText: workTimeText, + payText: payText, + isFavorite: vo.isFavorite + ) + } + + public static let `mock`: WorkerNativeEmployCardRO = .init( + showBiginnerTag: true, + titleText: "사울시 강남동", + timeDurationForWalkingText: "도보 15분 ~ 20분", + targetInfoText: "1등급 54세 여성", + workDaysText: "", + workTimeText: "월, 화, 수", + payText: "시급 5000원", + isFavorite: true + ) + + static func timeForDistance(meter: Int) -> String { + switch meter { + case 0..<200: + return "도보 5분 이내" + case 200..<400: + return "도보 5 ~ 10분" + case 400..<700: + return "도보 10 ~ 15분" + case 700..<1000: + return "도보 15 ~ 20분" + case 1000..<1250: + return "도보 20 ~ 25분" + case 1250..<1500: + return "도보 25 ~ 30분" + case 1500..<1750: + return "도보 30 ~ 35분" + case 1750..<2000: + return "도보 35 ~ 40분" + default: + return "도보 40분 ~" + } + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCard.swift new file mode 100644 index 00000000..75c0d8dd --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCard.swift @@ -0,0 +1,220 @@ +// +// WorkerWorknetEmployCard.swift +// DSKit +// +// Created by choijunios on 9/6/24. +// + +import UIKit +import RxSwift +import RxCocoa +import Entity + +public class WorkerWorknetEmployCard: VStack { + + // View + public let starButton: IconWithColorStateButton = { + let button = IconWithColorStateButton( + representImage: DSKitAsset.Icons.subscribeStar.image, + normalColor: DSKitAsset.Colors.gray200.color, + accentColor: DSKitAsset.Colors.orange300.color + ) + return button + }() + + + // tags + let worknetTag: TagLabel = { + let tag = TagLabel( + text: "워크넷", + typography: .caption, + textColor: hexStringToUIColor(hex: "#2B8BDC"), + backgroundColor: hexStringToUIColor(hex: "#D3EBFF") + ) + return tag + }() + let beginnerTag: TagLabel = { + let tag = TagLabel( + text: "초보가능", + typography: .caption, + textColor: DSKitAsset.Colors.orange500.color, + backgroundColor: DSKitAsset.Colors.orange100.color + ) + return tag + }() + let daysUntilDeadlineTag: TagLabel = { + let tag = TagLabel( + text: "", + typography: .caption, + textColor: DSKitAsset.Colors.orange500.color, + backgroundColor: DSKitAsset.Colors.orange100.color + ) + return tag + }() + + + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle2) + label.numberOfLines = 0 + return label + }() + + + let estimatedArrivalTimeLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.attrTextColor = DSKitAsset.Colors.gray500.color + label.numberOfLines = 0 + return label + }() + + + let workTimeLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.attrTextColor = DSKitAsset.Colors.gray500.color + label.numberOfLines = 0 + return label + }() + + + let payLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.attrTextColor = DSKitAsset.Colors.gray500.color + label.numberOfLines = 0 + return label + }() + + public init() { + super.init([], alignment: .fill) + setAppearance() + setLayout() + } + public required init(coder: NSCoder) { fatalError() } + + func setAppearance() { + self.backgroundColor = DSColor.gray0.color + } + + func setLayout() { + + // MARK: Tag & Star + let tagStack = HStack( + [ + worknetTag, + beginnerTag, + daysUntilDeadlineTag, + ], + spacing: 4 + ) + + let tagStarStack = HStack( + [ + tagStack, + Spacer(), + starButton + ], + alignment: .center, + distribution: .fill + ) + + // MARK: work days | work time + let timeImage = DSKitAsset.Icons.time.image.toView() + let workTimeStack = HStack( + [ + timeImage, + workTimeLabel + ], + alignment: .center, + distribution: .fill + ) + + // MARK: pay + let payImage = DSKitAsset.Icons.money.image.toView() + let paymentStack = HStack( + [ + payImage, + payLabel + ], + alignment: .center, + distribution: .fill + ) + + NSLayoutConstraint.activate([ + timeImage.widthAnchor.constraint(equalToConstant: 24), + timeImage.heightAnchor.constraint(equalTo: timeImage.widthAnchor), + payImage.widthAnchor.constraint(equalToConstant: 24), + payImage.heightAnchor.constraint(equalTo: payImage.widthAnchor), + starButton.widthAnchor.constraint(equalToConstant: 24), + starButton.heightAnchor.constraint(equalTo: starButton.widthAnchor), + ]) + + let viewList = [ + tagStarStack, + Spacer(height: 8), + titleLabel, + Spacer(height: 8), + VStack([ + estimatedArrivalTimeLabel, + Spacer(height: 4), + workTimeStack, + Spacer(height: 2), + paymentStack + ], alignment: .leading) + ] + + viewList.forEach { + self.addArrangedSubview($0) + } + } + + public func applyRO(ro: WorkerWorknetEmployCardRO) { + + beginnerTag.isHidden = !ro.showBeginnerTag + daysUntilDeadlineTag.textString = ro.leftDayUnitlDeadlineText + starButton.setState(ro.isStarred ? .accent : .normal) + titleLabel.textString = ro.titleText + estimatedArrivalTimeLabel.textString = ro.timeDurationForWalkingText + workTimeLabel.textString = ro.workTimeInfoText + payLabel.textString = ro.paymentInfoText + } +} + +func hexStringToUIColor(hex: String) -> UIColor { + var cString: String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + + if cString.hasPrefix("#") { + cString.remove(at: cString.startIndex) + } + + if cString.count != 6 { + return UIColor.gray // 기본 색상 + } + + var rgbValue: UInt64 = 0 + Scanner(string: cString).scanHexInt64(&rgbValue) + + return UIColor( + red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(rgbValue & 0x0000FF) / 255.0, + alpha: 1.0 + ) +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + let cardView = WorkerWorknetEmployCard() + + cardView.applyRO( + ro: .init( + showBeginnerTag: true, + leftDayUnitlDeadlineText: "D-10", + titleText: "[수원 매탄동] 방문요양 주 3회 (4등급 여자 어르신)", + timeDurationForWalkingText: "도보 15분~20분", + workTimeInfoText: "주 6일 근무 | (오전) 1시 00분 ~ (오후) 4시 00분 ", + paymentInfoText: "시급 9,860원 이상", + isStarred: true + ) + ) + + return cardView +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCardCell.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCardCell.swift new file mode 100644 index 00000000..bdbd6f89 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCardCell.swift @@ -0,0 +1,113 @@ +// +// WorkerWorknetEmployCardCell.swift +// DSKit +// +// Created by choijunios on 9/6/24. +// + +import UIKit +import RxSwift +import RxCocoa +import Entity + +public class WorkerWorknetEmployCardCell: UITableViewCell { + + public static let identifier = String(describing: WorkerWorknetEmployCardCell.self) + + private var disposables: [Disposable?]? + + let tappableArea: TappableUIView = { + let view = TappableUIView() + view.layer.borderWidth = 1 + view.layer.cornerRadius = 12 + view.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor + return view + }() + let cardView = WorkerWorknetEmployCard() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setAppearance() + setLayout() + } + public required init?(coder: NSCoder) { fatalError() } + + public override func prepareForReuse() { + disposables?.forEach { $0?.dispose() } + disposables = nil + } + + func setAppearance() { + contentView.backgroundColor = .clear + } + + func setLayout() { + + tappableArea.layoutMargins = .init(top: 16, left: 16, bottom: 16, right: 16) + + cardView.translatesAutoresizingMaskIntoConstraints = false + tappableArea.addSubview(cardView) + + + tappableArea.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(tappableArea) + + + NSLayoutConstraint.activate([ + cardView.topAnchor.constraint(equalTo: tappableArea.layoutMarginsGuide.topAnchor), + cardView.leftAnchor.constraint(equalTo: tappableArea.layoutMarginsGuide.leftAnchor), + cardView.rightAnchor.constraint(equalTo: tappableArea.layoutMarginsGuide.rightAnchor), + cardView.bottomAnchor.constraint(equalTo: tappableArea.layoutMarginsGuide.bottomAnchor), + + tappableArea.topAnchor.constraint(equalTo: contentView.topAnchor), + tappableArea.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + tappableArea.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + tappableArea.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), + ]) + } + + public func bind(postId: String, ro: WorkerWorknetEmployCardRO, viewModel: WorkerEmployCardViewModelable) { + + // 카드 컨텐츠 바인딩 + cardView.applyRO(ro: ro) + + let starButton = cardView.starButton + + let favoriteRequestResult = starButton + .onTapEvent + .map { state in + // normal인 경우 true / 즐겨찾기 요청 + state == .normal + } + .flatMap { [viewModel] isFavoriteRequest in + viewModel.setPostFavoriteState( + isFavoriteRequest: isFavoriteRequest, + postId: postId, + postType: .native + ) + } + + // input + let disposables: [Disposable?] = [ + + // Input + tappableArea + .rx.tap + .subscribe(onNext: { [weak viewModel] _ in + viewModel?.showPostDetail(postType: .workNet, id: postId) + }), + + favoriteRequestResult + .subscribe(onNext: { [starButton] isSuccess in + + if isSuccess { + + // 성공시 상태변경 + starButton.toggle() + } + }) + ] + + self.disposables = disposables + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCardRO.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCardRO.swift new file mode 100644 index 00000000..2717a78a --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorknetCard/WorkerWorknetEmployCardRO.swift @@ -0,0 +1,107 @@ +// +// WorkerWorknetEmployCardRO.swift +// DSKit +// +// Created by choijunios on 9/6/24. +// + +import Foundation +import Entity + +public struct WorkerWorknetEmployCardRO { + + public let showBeginnerTag: Bool + public let leftDayUnitlDeadlineText: String + public let titleText: String + public let timeDurationForWalkingText: String + public let workTimeInfoText: String + public let paymentInfoText: String + public let isStarred: Bool + + public init( + showBeginnerTag: Bool, + leftDayUnitlDeadlineText: String, + titleText: String, + timeDurationForWalkingText: String, + workTimeInfoText: String, + paymentInfoText: String, + isStarred: Bool + ) { + self.showBeginnerTag = showBeginnerTag + self.leftDayUnitlDeadlineText = leftDayUnitlDeadlineText + self.titleText = titleText + self.timeDurationForWalkingText = timeDurationForWalkingText + self.workTimeInfoText = workTimeInfoText + self.paymentInfoText = paymentInfoText + self.isStarred = isStarred + } + + public static func create(vo: WorknetRecruitmentPostDetailVO) -> WorkerWorknetEmployCardRO { + + var leftDayUnitlDeadlineText: String = "타임존 다름" + if let dateDiff = Calendar.current.dateComponents([.day], from: .now, to: vo.applyDeadline).day { + leftDayUnitlDeadlineText = "D-\(dateDiff)" + if dateDiff == 0 { + leftDayUnitlDeadlineText = "D-Day" + } + } + + let durationText = Self.timeForDistance(meter: vo.distance) + + return .init( + showBeginnerTag: false, + leftDayUnitlDeadlineText: leftDayUnitlDeadlineText, + titleText: vo.title, + timeDurationForWalkingText: durationText, + workTimeInfoText: "\(vo.workingSchedule) | \(vo.workingTime)", + paymentInfoText: vo.payInfo, + isStarred: vo.isFavorite + ) + } + + public static func create(vo: WorknetRecruitmentPostVO) -> WorkerWorknetEmployCardRO { + + var leftDayUnitlDeadlineText: String = "타임존 다름" + if let dateDiff = Calendar.current.dateComponents([.day], from: .now, to: vo.applyDeadline).day { + leftDayUnitlDeadlineText = "D-\(dateDiff)" + if dateDiff == 0 { + leftDayUnitlDeadlineText = "D-Day" + } + } + + let durationText = Self.timeForDistance(meter: vo.distance) + + return .init( + showBeginnerTag: false, + leftDayUnitlDeadlineText: leftDayUnitlDeadlineText, + titleText: vo.title, + timeDurationForWalkingText: durationText, + workTimeInfoText: "\(vo.workingSchedule) | \(vo.workingTime)", + paymentInfoText: vo.payInfo, + isStarred: vo.isFavorite + ) + } + + static func timeForDistance(meter: Int) -> String { + switch meter { + case 0..<200: + return "도보 5분 이내" + case 200..<400: + return "도보 5 ~ 10분" + case 400..<700: + return "도보 10 ~ 15분" + case 700..<1000: + return "도보 15 ~ 20분" + case 1000..<1250: + return "도보 20 ~ 25분" + case 1250..<1500: + return "도보 25 ~ 30분" + case 1500..<1750: + return "도보 30 ~ 35분" + case 1750..<2000: + return "도보 35 ~ 40분" + default: + return "도보 40분 ~" + } + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift index e1186f0b..7cdb9975 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift @@ -18,6 +18,7 @@ public class CenterInfoCardView: TappableUIView { // View let nameLabel: IdleLabel = { let label = IdleLabel(typography: .Subtitle3) + label.numberOfLines = 0 return label }() let locationLabel: IdleLabel = { @@ -26,6 +27,17 @@ public class CenterInfoCardView: TappableUIView { return label }() + public let locationImageView: UIImageView = { + let iamgeView = DSKitAsset.Icons.location.image.toView() + iamgeView.tintColor = DSColor.gray400.color + return iamgeView + }() + public let chevronLeftImage: UIImageView = { + let view = DSKitAsset.Icons.chevronRight.image.toView() + view.tintColor = DSKitAsset.Colors.gray200.color + return view + }() + // Observable private let disposeBag = DisposeBag() @@ -45,17 +57,8 @@ public class CenterInfoCardView: TappableUIView { } - public let chevronLeftImage: UIImageView = { - let view = DSKitAsset.Icons.chevronRight.image.toView() - view.tintColor = DSKitAsset.Colors.gray200.color - return view - }() - private func setLayout() { - let locationImageView = DSKitAsset.Icons.location.image.toView() - locationImageView.tintColor = DSColor.gray400.color - let locationStack = HStack( [ locationImageView, diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/WorkConditionDisplayingView.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/WorkConditionDisplayingView.swift index 861b5ff8..d95177f7 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/WorkConditionDisplayingView.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Center/OverView/WorkConditionDisplayingView.swift @@ -37,8 +37,9 @@ public class WorkConditionDisplayingView: HStack { public init() { super.init( - [keyStack, valueStack, Spacer()], - spacing: 32 + [keyStack, valueStack], + spacing: 32, + distribution: .fill ) setAppearance() setLayout() 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 843d9a28..204d0555 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 @@ -13,33 +13,48 @@ import Entity public class PostDetailForWorkerCoodinator: ChildCoordinator { public struct Dependency { + let postType: RecruitmentPostType let postId: String weak var parent: WorkerRecruitmentBoardCoordinatable? let navigationController: UINavigationController let recruitmentPostUseCase: RecruitmentPostUseCase + let workerProfileUseCase: WorkerProfileUseCase - public init(postId: String, parent: WorkerRecruitmentBoardCoordinatable? = nil, navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase) { + public init( + postType: RecruitmentPostType, + postId: String, + parent: WorkerRecruitmentBoardCoordinatable? = nil, + navigationController: UINavigationController, + recruitmentPostUseCase: RecruitmentPostUseCase, + workerProfileUseCase: WorkerProfileUseCase + ) { + self.postType = postType self.postId = postId self.parent = parent self.navigationController = navigationController self.recruitmentPostUseCase = recruitmentPostUseCase + self.workerProfileUseCase = workerProfileUseCase } } public weak var viewControllerRef: UIViewController? public weak var parent: WorkerRecruitmentBoardCoordinatable? + let postType: RecruitmentPostType let postId: String public let navigationController: UINavigationController let recruitmentPostUseCase: RecruitmentPostUseCase + let workerProfileUseCase: WorkerProfileUseCase public init( dependency: Dependency ) { + self.postType = dependency.postType self.postId = dependency.postId self.parent = dependency.parent self.navigationController = dependency.navigationController self.recruitmentPostUseCase = dependency.recruitmentPostUseCase + self.workerProfileUseCase = dependency.workerProfileUseCase } deinit { @@ -47,13 +62,32 @@ public class PostDetailForWorkerCoodinator: ChildCoordinator { } public func start() { - let vc = NativePostDetailForWorkerVC() - let vm = NativePostDetailForWorkerVM( - postId: postId, - coordinator: self, - recruitmentPostUseCase: recruitmentPostUseCase - ) - vc.bind(viewModel: vm) + + var vc: UIViewController! + + switch postType { + case .native: + let nativeDetailVC = NativePostDetailForWorkerVC() + let vm = NativePostDetailForWorkerVM( + postId: postId, + coordinator: self, + recruitmentPostUseCase: recruitmentPostUseCase, + workerProfileUseCase: workerProfileUseCase + ) + nativeDetailVC.bind(viewModel: vm) + vc = nativeDetailVC + case .workNet: + let worknetDetailVC = WorknetPostDetailForWorkerVC() + let vm = WorknetPostDetailForWorkerVM( + postId: postId, + coordinator: self, + recruitmentPostUseCase: recruitmentPostUseCase, + workerProfileUseCase: workerProfileUseCase + ) + worknetDetailVC.bind(viewModel: vm) + vc = worknetDetailVC + } + viewControllerRef = vc navigationController.pushViewController(vc, animated: true) } diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/NativePostDetailForWorkerVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/Postdetail/NativePostDetailForWorkerVC.swift similarity index 98% rename from project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/NativePostDetailForWorkerVC.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/Postdetail/NativePostDetailForWorkerVC.swift index 1aeca239..dfd8de35 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/NativePostDetailForWorkerVC.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/Postdetail/NativePostDetailForWorkerVC.swift @@ -253,14 +253,14 @@ public class NativePostDetailForWorkerVC: BaseViewController { public class PostDetailForWorkerContentView: UIView { /// 구인공고 카드 - let cardView: WorkerEmployCard = { - let view = WorkerEmployCard() + let cardView: WorkerNativeEmployCard = { + let view = WorkerNativeEmployCard() view.setToPostAppearance() return view }() /// 지도뷰 - public let workPlaceAndWorkerLocationView = WorkPlaceAndWorkerLocationView() + let workPlaceAndWorkerLocationView = WorkPlaceAndWorkerLocationView() /// 공고 상세정보들 let workConditionView = WorkConditionDisplayingView() diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/Postdetail/WorknetPostDetailForWorkerVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/Postdetail/WorknetPostDetailForWorkerVC.swift new file mode 100644 index 00000000..4a3166e2 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/Postdetail/WorknetPostDetailForWorkerVC.swift @@ -0,0 +1,387 @@ +// +// WorknetPostDetailForWorkerVC.swift +// BaseFeature +// +// Created by choijunios on 9/6/24. +// + +import UIKit +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class WorknetPostDetailForWorkerVC: BaseViewController { + + // Init + + // View + let navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar(innerViews: []) + bar.titleLabel.textString = "공고 정보" + return bar + }() + + // 구인공고 카드 + let cardView: WorkerWorknetEmployCard = { + let view = WorkerWorknetEmployCard() + return view + }() + + // 근무 장소(타이틀/설명1/걸어서~/지도) + let workPlaceAndWorkerLocationView = WorkPlaceAndWorkerLocationView() + + // 워크넷 링크 + var workNetPostLink: URL? + + public init() { + + super.init(nibName: nil, bundle: nil) + + setAppearance() + setLayout() + setObservable() + } + + // 모집 요강 + let recruitmentDetailTextView: MultiLineTextField = { + let field = MultiLineTextField(typography: .Body3) + field.isScrollEnabled = false + field.isUserInteractionEnabled = false + return field + }() + + // 근무 조건 + let workShapeLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + let workTimeLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + let paymentConditionLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + let workAddressLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + + + // 전형방법 + let applyDeadlineLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + let applyMethodLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + let submitMethodLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + let submitDocsLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.numberOfLines = 0 + return label + }() + + + // 기관 정보 + let centerInfoCard: CenterInfoCardView = { + let view = CenterInfoCardView() + view.isUserInteractionEnabled = false + view.chevronLeftImage.isHidden = true + return view + }() + + // 워크넷 링크 + let worknetLinkCard: CenterInfoCardView = { + let view = CenterInfoCardView() + view.locationImageView.isHidden = true + return view + }() + + public required init?(coder: NSCoder) { fatalError() } + + public func bind(viewModel: WorknetPostDetailForWorkerViewModelable) { + + super.bind(viewModel: viewModel) + + // Output + viewModel + .postDetail? + .drive(onNext: { + [weak self] detailVO in + + guard let self else { return } + + // card + let cardRO: WorkerWorknetEmployCardRO = .create(vo: detailVO) + cardView.applyRO(ro: cardRO) + + // 모집요강 + recruitmentDetailTextView.textString = detailVO.content + recruitmentDetailTextView.sizeToFit() + + // 근무조건 + workShapeLabel.textString = detailVO.workingSchedule + workTimeLabel.textString = detailVO.workingTime + paymentConditionLabel.textString = detailVO.payInfo + workAddressLabel.textString = detailVO.clientAddress + + // 전형방법 + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let applyDeadlineText = dateFormatter.string(from: detailVO.applyDeadline) + applyDeadlineLabel.textString = applyDeadlineText + applyMethodLabel.textString = detailVO.applyMethod + submitMethodLabel.textString = detailVO.recruitmentProcess + submitDocsLabel.textString = detailVO.requiredDocumentation + + // 기관정보 + centerInfoCard.bind( + nameText: detailVO.centerName, + locationText: detailVO.centerAddress + ) + + // 워크넷 링크 + worknetLinkCard.bind( + nameText: detailVO.title, + locationText: detailVO.jobPostingUrl + ) + workNetPostLink = URL(string: detailVO.jobPostingUrl) + }) + .disposed(by: disposeBag) + + viewModel + .starButtonRequestResult? + .drive(onNext: { [weak self] isSuccess in + + guard let self else { return } + + if isSuccess { + cardView.starButton.toggle() + } + }) + .disposed(by: disposeBag) + + // 위치정보 + if let locationInfo = viewModel.locationInfo?.asObservable().share() { + + locationInfo + .subscribe(onNext: { + [weak self] info in + // 위치정보 전달 + self?.workPlaceAndWorkerLocationView.bind(locationRO: info) + }) + .disposed(by: disposeBag) + + // 지도화면 클릭시 + workPlaceAndWorkerLocationView.mapViewBackGround + .rx.tap + .withLatestFrom(locationInfo) + .subscribe { [weak self] locationInfo in + let fullMapVC = WorkPlaceAndWorkerLocationFullVC() + fullMapVC.bind(locationRO: locationInfo) + self?.navigationController?.pushViewController(fullMapVC, animated: true) + } + .disposed(by: disposeBag) + } + + // Input + self.rx.viewWillAppear + .map({ _ in }) + .bind(to: viewModel.requestRefresh) + .disposed(by: disposeBag) + + cardView.starButton + .onTapEvent + .map { state in + // normal인 경우 true / 즐겨찾기 요청 + state == .normal + } + .bind(to: viewModel.starButtonClicked) + .disposed(by: disposeBag) + + // 뒤로가기 버튼 + navigationBar.backButton + .rx.tap + .bind(to: viewModel.backButtonClicked) + .disposed(by: disposeBag) + } + + func setAppearance() { + view.backgroundColor = DSColor.gray0.color + } + + func setLayout() { + + // 근무조건 + let workConditionComponentStackList = [ + ("근무 형태", workShapeLabel), + ("근무 시간", workTimeLabel), + ("임김 조건", paymentConditionLabel), + ("근무 주소", workAddressLabel), + ].map { (keyText, valueLabel) in + + let keyLabel = IdleLabel(typography: .Body2) + keyLabel.attrTextColor = DSColor.gray300.color + keyLabel.textString = keyText + keyLabel.textAlignment = .left + + return HStack([keyLabel, valueLabel], spacing: 32, alignment: .top) + } + + let workConditionStack: VStack = .init(workConditionComponentStackList, spacing: 8, alignment: .leading) + + // 전형방법 + let applyMethodComponentStackList = [ + ("접수 마감일", applyDeadlineLabel), + ("전형 방법", applyMethodLabel), + ("접수 방법", submitMethodLabel), + ("제출 서류", submitDocsLabel), + ].map { (keyText, valueLabel) in + + let keyLabel = IdleLabel(typography: .Body2) + keyLabel.attrTextColor = DSColor.gray300.color + keyLabel.textString = keyText + let labelWidth = keyLabel.intrinsicContentSize.width + let spacing = 114 - labelWidth + + return HStack([keyLabel, valueLabel], spacing: spacing, alignment: .top) + } + + let applyMethodComponentStack: VStack = .init(applyMethodComponentStackList, spacing: 8, alignment: .leading) + + let scrollView = UIScrollView() + let scvContentGuide = scrollView.contentLayoutGuide + let scvFrameGuide = scrollView.frameLayoutGuide + + let viewList = [ + + cardView, + + workPlaceAndWorkerLocationView, + + VStack([ + makeTitleLabel(text: "모집요강"), + recruitmentDetailTextView + ], spacing: 20, alignment: .fill), + + VStack([ + makeTitleLabel(text: "근무 조건"), + workConditionStack + ], spacing: 20, alignment: .fill), + + VStack([ + makeTitleLabel(text: "전형 방법"), + applyMethodComponentStack + ], spacing: 20, alignment: .fill), + + VStack([ + makeTitleLabel(text: "기관 정보"), + centerInfoCard + ], spacing: 20, alignment: .fill), + + VStack([ + makeTitleLabel(text: "워크넷 링크"), + worknetLinkCard + ], spacing: 20, alignment: .fill), + + ].map { containerView in + + let backgroundView = UIView() + backgroundView.backgroundColor = DSColor.gray0.color + backgroundView.layoutMargins = .init(top: 24, left: 20, bottom: 24, right: 20) + + containerView.translatesAutoresizingMaskIntoConstraints = false + backgroundView.addSubview(containerView) + + NSLayoutConstraint.activate([ + containerView.topAnchor.constraint(equalTo: backgroundView.layoutMarginsGuide.topAnchor), + containerView.leftAnchor.constraint(equalTo: backgroundView.layoutMarginsGuide.leftAnchor), + containerView.rightAnchor.constraint(equalTo: backgroundView.layoutMarginsGuide.rightAnchor), + containerView.bottomAnchor.constraint(equalTo: backgroundView.layoutMarginsGuide.bottomAnchor), + ]) + return backgroundView + } + + let scvContentStack = VStack(viewList, spacing: 8, alignment: .fill) + scvContentStack.backgroundColor = DSColor.gray050.color + + scvContentStack.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(scvContentStack) + + NSLayoutConstraint.activate([ + scvContentStack.topAnchor.constraint(equalTo: scvContentGuide.topAnchor), + scvContentStack.leftAnchor.constraint(equalTo: scvContentGuide.leftAnchor), + scvContentStack.rightAnchor.constraint(equalTo: scvContentGuide.rightAnchor), + scvContentStack.bottomAnchor.constraint(equalTo: scvContentGuide.bottomAnchor), + + scvContentStack.widthAnchor.constraint(equalTo: scvFrameGuide.widthAnchor) + ]) + + // MARK: view + [ + navigationBar, + scrollView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationBar.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + navigationBar.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), + + scrollView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor), + scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + func setObservable() { + + worknetLinkCard + .rx.tap + .subscribe(onNext: { [weak self] in + guard let self, let url = self.workNetPostLink else { return } + + let isSafaiOpended = openDeepLink(url: url) + + if !isSafaiOpended { + self.showAlert(vo: .init(title: "공고 확인 실패", message: "처리과정에서 문제가 발생했습니다.")) + } + }) + .disposed(by: disposeBag) + } + + private func openDeepLink(url: URL) -> Bool { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + return true + } + return false + } + + func makeTitleLabel(text: String) -> IdleLabel { + let label = IdleLabel(typography: .Subtitle1) + label.textAlignment = .left + label.textString = text + return label + } +} 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 bbc6d96f..3f581e3e 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 @@ -17,7 +17,7 @@ public struct WorkPlaceAndWorkerLocationMapRO { let workPlaceRoadAddress: String let homeToworkPlaceText: NSMutableAttributedString - let distanceToWorkPlaceText: String + let estimatedArrivalTimeText: String let workPlaceLocation: LocationInformation let workerLocation: LocationInformation? @@ -33,7 +33,7 @@ public class WorkPlaceAndWorkerLocationView: VStack { return label }() - let distanceLabel: IdleLabel = { + let estimatedArrivalTimeTextLabel: IdleLabel = { let label = IdleLabel(typography: .Subtitle2) label.textString = "" label.textAlignment = .left @@ -73,7 +73,7 @@ public class WorkPlaceAndWorkerLocationView: VStack { private func setLayout() { let walkingImage = DSKitAsset.Icons.walkingHuman.image.toView() - let timeCostStack = HStack([walkingImage, distanceLabel], spacing: 6, alignment: .center) + let timeCostStack = HStack([walkingImage, estimatedArrivalTimeTextLabel], spacing: 6, alignment: .center) let labelStack = VStack([walkToLocationLabel, timeCostStack], spacing: 4, alignment: .leading) @@ -107,7 +107,7 @@ public class WorkPlaceAndWorkerLocationView: VStack { public func bind(locationRO: WorkPlaceAndWorkerLocationMapRO) { walkToLocationLabel.attributedText = locationRO.homeToworkPlaceText - distanceLabel.textString = locationRO.distanceToWorkPlaceText + estimatedArrivalTimeTextLabel.textString = locationRO.estimatedArrivalTimeText mapView.bind( locationRO: locationRO, diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift index 6d093724..110e354d 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift @@ -37,6 +37,7 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork // Init private let postId: String private let recruitmentPostUseCase: RecruitmentPostUseCase + private let workerProfileUseCase: WorkerProfileUseCase // Ouput public var postForWorkerBundle: RxCocoa.Driver? @@ -55,12 +56,14 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork public init( postId: String, coordinator: PostDetailForWorkerCoodinator?, - recruitmentPostUseCase: RecruitmentPostUseCase + recruitmentPostUseCase: RecruitmentPostUseCase, + workerProfileUseCase: WorkerProfileUseCase ) { self.postId = postId self.coordinator = coordinator self.recruitmentPostUseCase = recruitmentPostUseCase + self.workerProfileUseCase = workerProfileUseCase super.init() @@ -72,28 +75,35 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork let getPostDetailResult = viewWillAppear .flatMap { [recruitmentPostUseCase] _ in recruitmentPostUseCase - .getPostDetailForWorker(id: postId) + .getNativePostDetailForWorker(id: postId) } .share() let getPostDetailSuccess = getPostDetailResult.compactMap { $0.value } let getPostDetailFailure = getPostDetailResult.compactMap { $0.error } - let getPostDetailFailureAlert = getPostDetailFailure - .map { error in - DefaultAlertContentVO( - title: "공고 불러오기 실패", - message: error.message - ) - } - postForWorkerBundle = getPostDetailSuccess.asDriver(onErrorRecover: { _ in fatalError() }) // MARK: 센터, 워커 위치정보 - locationInfo = getPostDetailSuccess - .map { [weak self] bundle in + let requestWorkerLocationResult = viewWillAppear + .flatMap({ [workerProfileUseCase] _ in + workerProfileUseCase + .getProfile(mode: .myProfile) + }) + + let requestWorkerLocationSuccess = requestWorkerLocationResult.compactMap { $0.value } + let requestWorkerLocationFailure = requestWorkerLocationResult.compactMap { $0.error } + + locationInfo = Observable + .zip(getPostDetailSuccess, requestWorkerLocationSuccess) + .map { + [weak self] bundle, profile in + // 요양보호사 위치 가져오기 - let workerLocation = self?.getWorkerLocation() + let workerLocation: LocationInformation = .init( + longitude: profile.longitude, + latitude: profile.latitude + ) let workPlaceLocation = bundle.jobLocation @@ -109,10 +119,12 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork let range = NSRange(text.range(of: roadAddress)!, in: text) attrText.addAttribute(.font, value: roadTextFont, range: range) + let estimatedArrivalTimeText = self?.timeForDistance(meter: bundle.distanceToWorkPlace) + return WorkPlaceAndWorkerLocationMapRO( workPlaceRoadAddress: roadAddress, homeToworkPlaceText: attrText, - distanceToWorkPlaceText: "\(bundle.distanceToWorkPlace)m", + estimatedArrivalTimeText: estimatedArrivalTimeText ?? "", workPlaceLocation: workPlaceLocation, workerLocation: workerLocation ) @@ -167,6 +179,22 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork message: error.message ) } + + let getPostDetailFailureAlert = getPostDetailFailure + .map { error in + DefaultAlertContentVO( + title: "공고 불러오기 실패", + message: error.message + ) + } + + let requestWorkerLocationFailureAlert = requestWorkerLocationFailure + .map { error in + DefaultAlertContentVO( + title: "요양보호사 위치정보 확인 실패", + message: error.message + ) + } // MARK: 즐겨찾기 starButtonRequestResult = starButtonClicked @@ -195,32 +223,33 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork Observable .merge( getPostDetailFailureAlert, - applyRequestFailureAlert + applyRequestFailureAlert, + requestWorkerLocationFailureAlert ) - .subscribe(self.alert) + .subscribe(onNext: { [weak self] alertVO in + guard let self else { return } + alert.onNext(alertVO) + }) .disposed(by: disposeBag) - // MARK: 로딩 Observable .merge(loadingStartObservables) - .subscribe(showLoading) + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + showLoading.onNext(()) + }) .disposed(by: disposeBag) Observable .merge(loadingEndObservables) .delay(.milliseconds(500), scheduler: MainScheduler.instance) - .subscribe(dismissLoading) + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + dismissLoading.onNext(()) + }) .disposed(by: disposeBag) } - - // MARK: Test - func getWorkerLocation() -> LocationInformation { - return .init( - longitude: 127.046425, - latitude: 37.504588 - ) - } public func setPostFavoriteState(isFavoriteRequest: Bool, postId: String, postType: Entity.RecruitmentPostType) -> RxSwift.Single { @@ -288,4 +317,27 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork } } } + + func timeForDistance(meter: Int) -> String { + switch meter { + case 0..<200: + return "도보 5분 이내" + case 200..<400: + return "도보 5 ~ 10분" + case 400..<700: + return "도보 10 ~ 15분" + case 700..<1000: + return "도보 15 ~ 20분" + case 1000..<1250: + return "도보 20 ~ 25분" + case 1250..<1500: + return "도보 25 ~ 30분" + case 1500..<1750: + return "도보 30 ~ 35분" + case 1750..<2000: + return "도보 35 ~ 40분" + default: + return "도보 40분 ~" + } + } } diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/WorknetPostDetailForWorkerVM.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/WorknetPostDetailForWorkerVM.swift new file mode 100644 index 00000000..349f53a0 --- /dev/null +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/WorknetPostDetailForWorkerVM.swift @@ -0,0 +1,254 @@ +// +// WorknetPostDetailForWorkerVM.swift +// BaseFeature +// +// Created by choijunios on 9/6/24. +// + +import UIKit +import RxCocoa +import RxSwift +import Entity +import PresentationCore +import UseCaseInterface +import DSKit + +public protocol WorknetPostDetailForWorkerViewModelable: BaseViewModel { + + // Output + var postDetail: Driver? { get } + var locationInfo: Driver? { get } + var starButtonRequestResult: Driver? { get } + + // Input + var requestRefresh: PublishRelay { get } + var backButtonClicked: PublishRelay { get } + var starButtonClicked: PublishRelay { get } +} + +public class WorknetPostDetailForWorkerVM: BaseViewModel, WorknetPostDetailForWorkerViewModelable { + + public weak var coordinator: PostDetailForWorkerCoodinator? + + // Init + private let postId: String + private let recruitmentPostUseCase: RecruitmentPostUseCase + private let workerProfileUseCase: WorkerProfileUseCase + + // Ouput + public var postDetail: RxCocoa.Driver? + public var locationInfo: RxCocoa.Driver? + public var starButtonRequestResult: Driver? + + // Input + public var requestRefresh: RxRelay.PublishRelay = .init() + public var backButtonClicked: RxRelay.PublishRelay = .init() + public var starButtonClicked: RxRelay.PublishRelay = .init() + + + public init( + postId: String, + coordinator: PostDetailForWorkerCoodinator?, + recruitmentPostUseCase: RecruitmentPostUseCase, + workerProfileUseCase: WorkerProfileUseCase + ) + { + self.postId = postId + self.coordinator = coordinator + self.recruitmentPostUseCase = recruitmentPostUseCase + self.workerProfileUseCase = workerProfileUseCase + + super.init() + + let getPostDetailResult = requestRefresh + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase + .getWorknetPostDetailForWorker(id: postId) + } + .share() + + let getPostDetailSuccess = getPostDetailResult.compactMap { $0.value } + let getPostDetailFailure = getPostDetailResult.compactMap { $0.error } + + let getPostDetailFailureAlert = getPostDetailFailure + .map { [weak self] error in + DefaultAlertContentVO( + title: "공고 불러오기 실패", + message: error.message + ) { [weak self] in + self?.coordinator?.coordinatorDidFinish() + } + } + + postDetail = getPostDetailSuccess + .asDriver(onErrorDriveWith: .never()) + + // MARK: 센터, 워커 위치정보 + let requestWorkerLocationResult = requestRefresh + .flatMap({ [workerProfileUseCase] _ in + workerProfileUseCase + .getProfile(mode: .myProfile) + }) + + let requestWorkerLocationSuccess = requestWorkerLocationResult.compactMap { $0.value } + let requestWorkerLocationFailure = requestWorkerLocationResult.compactMap { $0.error } + + locationInfo = Observable + .zip(getPostDetailSuccess, requestWorkerLocationSuccess) + .map { [weak self] postVO, profile in + + // 요양보호사 위치 가져오기 + let workerLocation: LocationInformation = .init( + longitude: profile.longitude, + latitude: profile.latitude + ) + + let workPlaceLocation: LocationInformation = .init( + longitude: Double(postVO.longitude) ?? 0.0, + latitude: Double(postVO.latitude) ?? 0.0 + ) + + let roadAddress = postVO.clientAddress + let text = "거주지에서 \(roadAddress) 까지" + var normalAttr = Typography.Body2.attributes + normalAttr[.foregroundColor] = DSKitAsset.Colors.gray500.color + + let attrText = NSMutableAttributedString(string: text, attributes: normalAttr) + + let roadTextFont = Typography.Subtitle3.attributes[.font]! + + let range = NSRange(text.range(of: roadAddress)!, in: text) + attrText.addAttribute(.font, value: roadTextFont, range: range) + + let estimatedArrivalTimeText = self?.timeForDistance(meter: postVO.distance) + + return WorkPlaceAndWorkerLocationMapRO( + workPlaceRoadAddress: roadAddress, + homeToworkPlaceText: attrText, + estimatedArrivalTimeText: estimatedArrivalTimeText ?? "", + workPlaceLocation: workPlaceLocation, + workerLocation: workerLocation + ) + } + .asDriver(onErrorRecover: { _ in fatalError() }) + + // MARK: 버튼 처리 + backButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + self.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + // MARK: 즐겨찾기 + starButtonRequestResult = starButtonClicked + .flatMap { [weak self] isFavoriteRequest in + self?.setPostFavoriteState( + isFavoriteRequest: isFavoriteRequest, + postId: postId, + postType: .native + ) ?? .never() + } + .asDriver(onErrorJustReturn: false) + + + // MARK: Alert + Observable + .merge(getPostDetailFailureAlert) + .subscribe(onNext: { [weak self] alertVO in + guard let self else { return } + alert.onNext(alertVO) + }) + .disposed(by: disposeBag) + } + + public func setPostFavoriteState(isFavoriteRequest: Bool, postId: String, postType: Entity.RecruitmentPostType) -> RxSwift.Single { + + return Single.create { [weak self] observer in + + guard let self else { return Disposables.create { } } + + let observable: Single>! + + // 로딩화면 시작 + self.showLoading.onNext(()) + + if isFavoriteRequest { + + observable = recruitmentPostUseCase + .addFavoritePost( + postId: postId, + type: postType + ) + + } else { + + observable = recruitmentPostUseCase + .removeFavoritePost(postId: postId) + } + + let reuslt = observable + .asObservable() + .map({ [weak self] result in + + // 로딩화면 종료 + self?.dismissLoading.onNext(()) + + return result + }) + .share() + + let success = reuslt.compactMap { $0.value } + let failure = reuslt.compactMap { $0.error } + + let failureAlertDisposable = failure.map { error in + DefaultAlertContentVO( + title: "즐겨찾기 오류", + message: error.message + ) + } + .asObservable() + .subscribe(onNext: { [weak self] alertVO in + guard let self else { return } + alert.onNext(alertVO) + }) + + + let disposable = Observable + .merge( + success.map { _ in true }.asObservable(), + failure.map { _ in false }.asObservable() + ) + .asSingle() + .subscribe(observer) + + return Disposables.create { + disposable.dispose() + failureAlertDisposable.dispose() + } + } + } + + func timeForDistance(meter: Int) -> String { + switch meter { + case 0..<200: + return "도보 5분 이내" + case 200..<400: + return "도보 5 ~ 10분" + case 400..<700: + return "도보 10 ~ 15분" + case 700..<1000: + return "도보 15 ~ 20분" + case 1000..<1250: + return "도보 20 ~ 25분" + case 1250..<1500: + return "도보 25 ~ 30분" + case 1500..<1750: + return "도보 30 ~ 35분" + case 1750..<2000: + return "도보 35 ~ 40분" + default: + return "도보 40분 ~" + } + } +} 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 4d6d3455..1822595a 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 @@ -70,6 +70,9 @@ public extension BaseViewController { ) let close = UIAlertAction(title: "닫기", style: .default) { [weak self] _ in self?.alert(vo: vo) + + // dismiss + vo.onDismiss?() } alert.addAction(close) present(alert, animated: true, completion: nil) diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift index 7d214be1..f2e066d4 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift @@ -30,8 +30,8 @@ public class PostDetailForCenterVC: BaseViewController { return bar }() - let sampleCard: WorkerEmployCard = { - let card = WorkerEmployCard() + let sampleCard: WorkerNativeEmployCard = { + let card = WorkerNativeEmployCard() card.starButton.isHidden = true return card }() 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 8d5e3185..ff32ca00 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 @@ -76,8 +76,8 @@ public class PostOverviewVC: BaseViewController { return label }() - let sampleCard: WorkerEmployCard = { - let card = WorkerEmployCard() + let sampleCard: WorkerNativeEmployCard = { + let card = WorkerNativeEmployCard() card.starButton.isUserInteractionEnabled = false return card }() diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift index bb14b3e0..2c4cd58d 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift @@ -146,7 +146,11 @@ public class CenterRecruitmentPostBoardVM: BaseViewModel, CenterRecruitmentPostB message: error.message ) } - .subscribe(alert) + .subscribe(onNext: { [weak self] alertVO in + guard let self else { return } + + alert.onNext(alertVO) + }) .disposed(by: disposeBag) } diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift index 4200f4db..58c1e5b4 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift @@ -73,9 +73,9 @@ public class InitialScreenVM: BaseViewModel { // 센터관리자 확인 printIfDebug("☑️ 센터관리자 정보를 확인합니다.") - // 저장된 센터정보가 없는 경우 + // 센터프로필 조회 및 refresh 확인 let requestCenterInfoResult = centerProfileUseCase - .getProfile(mode: .myProfile) + .getFreshProfile(mode: .myProfile) .asObservable() .share() let success = requestCenterInfoResult.compactMap { $0.value } @@ -118,9 +118,9 @@ public class InitialScreenVM: BaseViewModel { .disposed(by: disposeBag) } else { - // 요양보호사 확인 + // 요양보호사프로필 조회 및 refresh 확인 let requestWorkerInfoResult = workerProfileUseCase - .getProfile(mode: .myProfile) + .getFreshProfile(mode: .myProfile) .asObservable() .share() let success = requestWorkerInfoResult.compactMap { $0.value } 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 index 984800fa..cb07e86e 100644 --- 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 @@ -15,15 +15,11 @@ import DSKit public class WorkerPagablePostBoardVC: BaseViewController { - typealias Cell = WorkerNativeEmployCardCell + typealias NativeCell = WorkerNativeEmployCardCell + typealias WorknetCell = WorkerWorknetEmployCardCell // View - let postTableView: UITableView = { - let tableView = UITableView() - tableView.rowHeight = UITableView.automaticDimension - tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) - return tableView - }() + let postTableView = UITableView() let tableHeader = BoardSortigHeaderView() @@ -31,7 +27,7 @@ public class WorkerPagablePostBoardVC: BaseViewController { var isPaging = true // Observable - let cellData: BehaviorRelay<[PostBoardCellData]> = .init(value: []) + var postData: [RecruitmentPostForWorkerRepresentable] = [] let requestNextPage: PublishRelay = .init() public init() { @@ -49,6 +45,12 @@ public class WorkerPagablePostBoardVC: BaseViewController { } private func setTableView() { + + postTableView.rowHeight = UITableView.automaticDimension + postTableView.estimatedRowHeight = 218 + postTableView.register(NativeCell.self, forCellReuseIdentifier: NativeCell.identifier) + postTableView.register(WorknetCell.self, forCellReuseIdentifier: WorknetCell.identifier) + postTableView.dataSource = self postTableView.delegate = self postTableView.separatorStyle = .none @@ -94,9 +96,9 @@ public class WorkerPagablePostBoardVC: BaseViewController { // Output viewModel .postBoardData? - .drive(onNext: { [weak self] (isRefreshed, cellData) in + .drive(onNext: { [weak self] (isRefreshed, postData) in guard let self else { return } - self.cellData.accept(cellData) + self.postData = postData self.postTableView.reloadData() isPaging = false @@ -128,7 +130,7 @@ public class WorkerPagablePostBoardVC: BaseViewController { .postBoardData? .drive(onNext: { [weak self] (isRefreshed, cellData) in guard let self else { return } - self.cellData.accept(cellData) + self.postData = postData self.postTableView.reloadData() isPaging = false @@ -153,20 +155,48 @@ public class WorkerPagablePostBoardVC: BaseViewController { extension WorkerPagablePostBoardVC: UITableViewDataSource, UITableViewDelegate { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - cellData.value.count + postData.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 as? WorkerNativeEmployCardViewModelable { - cell.bind(postId: cellData.postId, vo: cellData.cardVO, viewModel: vm) + var cell: UITableViewCell! + + let postData = postData[indexPath.row] + + switch postData.postType { + case .native: + + // Cell + let nativeCell = tableView.dequeueReusableCell(withIdentifier: NativeCell.identifier) as! NativeCell + + let postVO = postData as! NativeRecruitmentPostForWorkerVO + let vm = viewModel as! WorkerEmployCardViewModelable + let cellVO: WorkerNativeEmployCardVO = .create(vo: postVO) + nativeCell.bind( + postId: postVO.postId, + vo: cellVO, + viewModel: vm + ) + cell = nativeCell + case .workNet: + + // Cell + let workNetCell = tableView.dequeueReusableCell(withIdentifier: WorknetCell.identifier) as! WorknetCell + + let postVO = postData as! WorknetRecruitmentPostVO + let vm = viewModel as! WorkerEmployCardViewModelable + let cellRO = WorkerWorknetEmployCardRO.create(vo: postVO) + workNetCell.bind( + postId: postVO.id, + ro: cellRO, + viewModel: vm + ) + cell = workNetCell } + cell.selectionStyle = .none + 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 338ebc6b..ee8ae4e9 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,26 +15,22 @@ import DSKit public class WorkerRecruitmentPostBoardVC: BaseViewController { - typealias Cell = WorkerNativeEmployCardCell + typealias NativeCell = WorkerNativeEmployCardCell + typealias WorknetCell = WorkerWorknetEmployCardCell // View fileprivate let topContainer: WorkerMainTopContainer = { let container = WorkerMainTopContainer(innerViews: []) return container }() - let postTableView: UITableView = { - let tableView = UITableView() - tableView.rowHeight = UITableView.automaticDimension - tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) - return tableView - }() + let postTableView = UITableView() let tableHeader = BoardSortigHeaderView() // Paging var isPaging = true // Observable - let cellData: BehaviorRelay<[PostBoardCellData]> = .init(value: []) + var postData: [RecruitmentPostForWorkerRepresentable] = [] let requestNextPage: PublishRelay = .init() public init() { @@ -63,9 +59,9 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { viewModel .postBoardData? - .drive(onNext: { [weak self] (isRefreshed: Bool, cellData) in + .drive(onNext: { [weak self] (isRefreshed: Bool, postData) in guard let self else { return } - self.cellData.accept(cellData) + self.postData = postData postTableView.reloadData() isPaging = false @@ -100,6 +96,12 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { } private func setTableView() { + + postTableView.rowHeight = UITableView.automaticDimension + postTableView.estimatedRowHeight = 218 + postTableView.register(NativeCell.self, forCellReuseIdentifier: NativeCell.identifier) + postTableView.register(WorknetCell.self, forCellReuseIdentifier: WorknetCell.identifier) + postTableView.dataSource = self postTableView.delegate = self postTableView.separatorStyle = .none @@ -143,20 +145,48 @@ public class WorkerRecruitmentPostBoardVC: BaseViewController { extension WorkerRecruitmentPostBoardVC: UITableViewDataSource, UITableViewDelegate { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - cellData.value.count + postData.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell - cell.selectionStyle = .none + var cell: UITableViewCell! - let cellData = cellData.value[indexPath.row] + let postData = postData[indexPath.row] - if let vm = viewModel as? WorkerNativeEmployCardViewModelable { - cell.bind(postId: cellData.postId, vo: cellData.cardVO, viewModel: vm) + switch postData.postType { + case .native: + + // Cell + let nativeCell = tableView.dequeueReusableCell(withIdentifier: NativeCell.identifier) as! NativeCell + + let postVO = postData as! NativeRecruitmentPostForWorkerVO + let vm = viewModel as! WorkerEmployCardViewModelable + let cellVO: WorkerNativeEmployCardVO = .create(vo: postVO) + nativeCell.bind( + postId: postVO.postId, + vo: cellVO, + viewModel: vm + ) + cell = nativeCell + case .workNet: + + // Cell + let workNetCell = tableView.dequeueReusableCell(withIdentifier: WorknetCell.identifier) as! WorknetCell + + let postVO = postData as! WorknetRecruitmentPostVO + let vm = viewModel as! WorkerEmployCardViewModelable + let cellRO = WorkerWorknetEmployCardRO.create(vo: postVO) + workNetCell.bind( + postId: postVO.id, + ro: cellRO, + viewModel: vm + ) + cell = workNetCell } + cell.selectionStyle = .none + 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 aa49e317..230ceb3b 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift @@ -23,7 +23,7 @@ public class AppliedPostBoardVM: BaseViewModel, WorkerPagablePostBoardVMable { public var applyButtonClicked: RxRelay.PublishRelay<(postId: String, postTitle: String)> = .init() // Output - public var postBoardData: RxCocoa.Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? + public var postBoardData: RxCocoa.Driver? // Init public weak var coordinator: WorkerRecruitmentBoardCoordinatable? @@ -33,7 +33,7 @@ public class AppliedPostBoardVM: BaseViewModel, WorkerPagablePostBoardVMable { /// 값이 nil이라면 요청을 보내지 않습니다. var nextPagingRequest: PostPagingRequestForWorker? /// 가장최신의 데이터를 홀드, 다음 요청시 해당데이터에 새로운 데이터를 더해서 방출 - private let currentPostVO: BehaviorRelay<[NativeRecruitmentPostForWorkerVO]> = .init(value: []) + private let currentPostVO: BehaviorRelay<[RecruitmentPostForWorkerRepresentable]> = .init(value: []) public init(coordinator: WorkerRecruitmentBoardCoordinatable, recruitmentPostUseCase: RecruitmentPostUseCase) { self.coordinator = coordinator @@ -93,7 +93,7 @@ public class AppliedPostBoardVM: BaseViewModel, WorkerPagablePostBoardVMable { currentPostVO, requestPostListSuccess ) - .compactMap { [weak self] (prevPostList, fetchedData) -> (Bool, [PostBoardCellData])? in + .compactMap { [weak self] (prevPostList, fetchedData) -> BoardRefreshResult? in guard let self else { return nil } @@ -119,15 +119,7 @@ public class AppliedPostBoardVM: BaseViewModel, WorkerPagablePostBoardVMable { // 최근값 업데이트 self.currentPostVO.accept(mergedPosts) - // cellData 생성 - let cellData: [PostBoardCellData] = mergedPosts.map { vo in - - let cardVO: WorkerNativeEmployCardVO = .create(vo: vo) - - return .init(postId: vo.postId, cardVO: cardVO) - } - - return (isRefreshed, cellData) + return (isRefreshed, mergedPosts) } .asDriver(onErrorDriveWith: .never()) 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 d3ed925d..146f3e8e 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,13 +14,10 @@ import Entity import DSKit import UseCaseInterface -public struct PostBoardCellData { - let postId: String - let cardVO: WorkerNativeEmployCardVO -} +public typealias BoardRefreshResult = (isRefreshed: Bool, postData: [RecruitmentPostForWorkerRepresentable]) /// 페이징 보드 -public protocol WorkerPagablePostBoardVMable: BaseViewModel, WorkerNativeEmployCardViewModelable { +public protocol WorkerPagablePostBoardVMable: BaseViewModel, AppliableWorkerEmployCardVMable { var coordinator: WorkerRecruitmentBoardCoordinatable? { get } @@ -33,7 +30,7 @@ public protocol WorkerPagablePostBoardVMable: BaseViewModel, WorkerNativeEmployC var requestInitialPageRequest: PublishRelay { get } /// 페이지요청에 대한 결과를 전달합니다. - var postBoardData: Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? { get } + var postBoardData: Driver? { get } } /// 페이징 + 지원하기 @@ -55,7 +52,7 @@ public protocol WorkerRecruitmentPostBoardVMable: WorkerAppliablePostBoardVMable public class WorkerRecruitmentPostBoardVM: BaseViewModel, WorkerRecruitmentPostBoardVMable { // Output - public var postBoardData: Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? + public var postBoardData: Driver<(isRefreshed: Bool, postData: [RecruitmentPostForWorkerRepresentable])>? public var workerLocationTitleText: Driver? public var idleAlertVM: RxCocoa.Driver? @@ -75,7 +72,7 @@ public class WorkerRecruitmentPostBoardVM: BaseViewModel, WorkerRecruitmentPostB /// 값이 nil이라면 요청을 보내지 않습니다. var nextPagingRequest: PostPagingRequestForWorker? = .initial /// 가장최신의 데이터를 홀드, 다음 요청시 해당데이터에 새로운 데이터를 더해서 방출 - private let currentPostVO: BehaviorRelay<[NativeRecruitmentPostForWorkerVO]> = .init(value: []) + private let currentPostVO: BehaviorRelay<[RecruitmentPostForWorkerRepresentable]> = .init(value: []) // Observable let dispostBag = DisposeBag() @@ -208,7 +205,7 @@ public class WorkerRecruitmentPostBoardVM: BaseViewModel, WorkerRecruitmentPostB currentPostVO, requestPostListSuccess ) - .compactMap { [weak self] (prevPostList, fetchedData) -> (Bool, [PostBoardCellData])? in + .compactMap { [weak self] (prevPostList, fetchedData) -> BoardRefreshResult? in guard let self else { return nil } @@ -254,14 +251,7 @@ public class WorkerRecruitmentPostBoardVM: BaseViewModel, WorkerRecruitmentPostB // 최근값 업데이트 self.currentPostVO.accept(mergedPosts) - // cellData생성 - let cellData: [PostBoardCellData] = mergedPosts.map { postVO in - - let cardVO: WorkerNativeEmployCardVO = .create(vo: postVO) - return .init(postId: postVO.postId, cardVO: cardVO) - } - - return (isRefreshed, cellData) + return (isRefreshed, mergedPosts) } .asDriver(onErrorDriveWith: .never()) @@ -312,8 +302,8 @@ public class WorkerRecruitmentPostBoardVM: BaseViewModel, WorkerRecruitmentPostB // MARK: WorkerPagablePostBoardVMable + Extension extension WorkerPagablePostBoardVMable { - public func showPostDetail(id: String) { - coordinator?.showPostDetail(postId: id) + public func showPostDetail(postType: RecruitmentPostType, id: String) { + coordinator?.showPostDetail(postType: postType, postId: id) } public func setPostFavoriteState(isFavoriteRequest: Bool, postId: String, postType: Entity.RecruitmentPostType) -> RxSwift.Single { 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 6af08e59..756f7041 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift @@ -22,117 +22,60 @@ public class StarredPostBoardVM: BaseViewModel, WorkerAppliablePostBoardVMable { public var applyButtonClicked: RxRelay.PublishRelay<(postId: String, postTitle: String)> = .init() // Output - public var postBoardData: RxCocoa.Driver<(isRefreshed: Bool, cellData: [PostBoardCellData])>? + public var postBoardData: RxCocoa.Driver? public var idleAlertVM: RxCocoa.Driver? // Init public weak var coordinator: WorkerRecruitmentBoardCoordinatable? public let 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 super.init() - var loadingStartObservables: [Observable] = [] - var loadingEndObservables: [Observable] = [] - // MARK: 공고리스트 처음부터 요청하기 - let initialRequest = requestInitialPageRequest - .flatMap { [weak self, recruitmentPostUseCase] request in + let initialRequestResult = requestInitialPageRequest + .flatMap { [weak self, recruitmentPostUseCase] _ in - self?.currentPostVO.accept([]) - self?.nextPagingRequest = .initial + self?.showLoading.onNext(()) return recruitmentPostUseCase - .getFavoritePostListForWorker( - request: .initial, - postCount: 10 - ) - } - .share() - - // 로딩 시작 - loadingStartObservables.append(initialRequest.map { _ in }) - - // MARK: 공고리스트 페이징 요청 - let pagingRequest = requestNextPage - .compactMap { [weak self] _ in - // 요청이 없는 경우 요청을 보내지 않는다. - // ThirdPatry에서도 불러올 데이터가 없는 경우입니다. - self?.nextPagingRequest - } - .flatMap { [recruitmentPostUseCase] request in - recruitmentPostUseCase - .getFavoritePostListForWorker( - request: request, - postCount: 10 - ) + .getFavoritePostListForWorker() } - - let postPageReqeustResult = Observable - .merge(initialRequest, pagingRequest) .share() - // 로딩 종료 - loadingEndObservables.append(postPageReqeustResult.map { _ in }) + initialRequestResult + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + dismissLoading.onNext(()) + }) + .disposed(by: disposeBag) - let requestPostListSuccess = postPageReqeustResult.compactMap { $0.value } - let requestPostListFailure = postPageReqeustResult.compactMap { $0.error } + let initialRequestSuccess = initialRequestResult.compactMap { $0.value } + let initialRequestFailure = initialRequestResult.compactMap { $0.error } - postBoardData = Observable - .zip( - currentPostVO, - requestPostListSuccess - ) - .compactMap { [weak self] (prevPostList, fetchedData) -> (Bool, [PostBoardCellData])? in - - 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) + postBoardData = initialRequestSuccess + .map({ list in - // 최근값 업데이트 - self.currentPostVO.accept(mergedPosts) - - // cellData 생성 - let cellData: [PostBoardCellData] = mergedPosts.map { vo in + let sortedList = list.sorted { lhs, rhs in + guard let lhsDate = lhs.beFavoritedTime, let rhsDate = rhs.beFavoritedTime else { + return false + } - let cardVO: WorkerNativeEmployCardVO = .create(vo: vo) + // 최신값을 배열의 앞쪽(화면의 상단)에 노출 - return .init(postId: vo.postId, cardVO: cardVO) + return lhsDate > rhsDate } - return (isRefreshed, cellData) - } + return (true, sortedList) + }) .asDriver(onErrorDriveWith: .never()) // MARK: 지원하기 let applyRequest: PublishRelay = .init() + self.idleAlertVM = applyButtonClicked .map { (postId: String, postTitle: String) in DefaultIdleAlertVM( @@ -144,20 +87,18 @@ public class StarredPostBoardVM: BaseViewModel, WorkerAppliablePostBoardVMable { } } .asDriver(onErrorDriveWith: .never()) - - // 로딩 시작 - loadingStartObservables.append(applyRequest.map { _ in }) let applyRequestResult = applyRequest - .flatMap { [recruitmentPostUseCase] postId in + .flatMap { [weak self, recruitmentPostUseCase] postId in + + self?.showLoading.onNext(()) + // 리스트화면에서는 앱내 지원만 지원합니다. - recruitmentPostUseCase + return recruitmentPostUseCase .applyToPost(postId: postId, method: .app) } .share() - // 로딩 종료 - loadingEndObservables.append(applyRequestResult.map { _ in }) let applyRequestFailure = applyRequestResult.compactMap { $0.error } @@ -169,7 +110,7 @@ public class StarredPostBoardVM: BaseViewModel, WorkerAppliablePostBoardVMable { ) } - let requestPostListFailureAlert = requestPostListFailure + let requestPostListFailureAlert = initialRequestFailure .map { error in DefaultAlertContentVO( title: "즐겨찾기한 공고 불러오기 오류", @@ -179,19 +120,22 @@ public class StarredPostBoardVM: BaseViewModel, WorkerAppliablePostBoardVMable { Observable .merge(applyRequestFailureAlert, requestPostListFailureAlert) - .subscribe(self.alert) + .subscribe(onNext: { [weak self] alertVO in + guard let self else { return } + alert.onNext(alertVO) + }) .disposed(by: disposeBag) - // MARK: 로딩 + // MARK: 로딩 종료 Observable - .merge(loadingStartObservables) - .subscribe(self.showLoading) - .disposed(by: disposeBag) - - Observable - .merge(loadingEndObservables) - .delay(.milliseconds(300), scheduler: MainScheduler.instance) - .subscribe(self.dismissLoading) + .merge( + initialRequestResult.map({ _ in }), + applyRequestResult.map({ _ in }) + ) + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + dismissLoading.onNext(()) + }) .disposed(by: disposeBag) } } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Worker/WorkerRecruitmentBoardCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Worker/WorkerRecruitmentBoardCoordinatable.swift index 721df7cd..bfd1a779 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Worker/WorkerRecruitmentBoardCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Worker/WorkerRecruitmentBoardCoordinatable.swift @@ -6,10 +6,11 @@ // import Foundation +import Entity public protocol WorkerRecruitmentBoardCoordinatable: ParentCoordinator { /// 요양보호사가 볼 수 있는 공고 상세정보를 표시합니다. - func showPostDetail(postId: String) + func showPostDetail(postType: RecruitmentPostType, postId: String) /// 센터 프로필을 표시합니다. func showCenterProfile(centerId: String)