diff --git a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift index cde09d1e..439905b0 100644 --- a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift +++ b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift @@ -32,9 +32,12 @@ public struct DomainAssembly: Assembly { } container.register(CenterProfileUseCase.self) { resolver in - let repository = resolver.resolve(UserProfileRepository.self)! - - return DefaultCenterProfileUseCase(repository: repository) + let userProfileRepository = resolver.resolve(UserProfileRepository.self)! + let userInfoLocalRepository = resolver.resolve(UserInfoLocalRepository.self)! + return DefaultCenterProfileUseCase( + userProfileRepository: userProfileRepository, + userInfoLocalRepository: userInfoLocalRepository + ) } container.register(RecruitmentPostUseCase.self) { resolver in @@ -46,9 +49,12 @@ public struct DomainAssembly: Assembly { } container.register(WorkerProfileUseCase.self) { resolver in - let repository = resolver.resolve(UserProfileRepository.self)! - - return DefaultWorkerProfileUseCase(repository: repository) + let userProfileRepository = resolver.resolve(UserProfileRepository.self)! + let userInfoLocalRepository = resolver.resolve(UserInfoLocalRepository.self)! + return DefaultWorkerProfileUseCase( + userProfileRepository: userProfileRepository, + userInfoLocalRepository: userInfoLocalRepository + ) } container.register(SettingScreenUseCase.self) { resolver in diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/RecruitmentManagementCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/RecruitmentManagementCoordinator.swift index 5d41b4c0..0c6f93f3 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/RecruitmentManagementCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/RecruitmentManagementCoordinator.swift @@ -45,7 +45,10 @@ public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatabl public func start() { let coordinator = CenterRecruitmentPostBoardScreenCoordinator( - navigationController: navigationController + dependency: .init( + navigationController: navigationController, + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + ) ) addChildCoordinator(coordinator) coordinator.parent = self @@ -63,8 +66,9 @@ public extension RecruitmentManagementCoordinator { func showCheckingApplicantScreen(postId: String) { let coordinator = CheckApplicantCoordinator( dependency: .init( + postId: postId, navigationController: navigationController, - centerEmployCardVO: .mock, + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self), workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self) ) ) @@ -73,12 +77,12 @@ public extension RecruitmentManagementCoordinator { coordinator.start() } - func showPostDetailScreenForCenter(postId: String, applicantCount: Int?) { + func showPostDetailScreenForCenter(postId: String, postState: PostState) { let coordinator = PostDetailForCenterCoordinator( dependency: .init( postId: postId, - applicantCount: applicantCount, + postState: postState, navigationController: navigationController, recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) diff --git a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift index f4842ccb..20e3ad76 100644 --- a/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift +++ b/project/Projects/Data/ConcreteRepository/RecruitmentPost/DefaultRecruitmentPostRepository.swift @@ -28,7 +28,7 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { let encodedData = try! JSONEncoder().encode(bundle.toDTO()) return service.request(api: .registerPost(postData: encodedData), with: .withToken) - .map { _ in () } + .mapToVoid() } public func getPostDetailForCenter(id: String) -> RxSwift.Single { @@ -50,6 +50,44 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { ).map { _ in () } } + public func getOngoingPosts() -> RxSwift.Single<[Entity.RecruitmentPostInfoForCenterVO]> { + return service.request(api: .getOnGoingPosts, with: .withToken) + .map(RecruitmentPostForCenterListDTO.self) + .map({ $0.jobPostings.map { $0.toVO() } }) + } + + public func getClosedPosts() -> RxSwift.Single<[Entity.RecruitmentPostInfoForCenterVO]> { + return service.request(api: .getClosedPosts, with: .withToken) + .map(RecruitmentPostForCenterListDTO.self) + .map({ $0.jobPostings.map { $0.toVO() } }) + } + + public func getPostApplicantCount(id: String) -> RxSwift.Single { + service.request(api: .getPostApplicantCount(id: id), with: .withToken) + .map(PostApplicantCountDTO.self) + .map { dto in + dto.applicantCount + } + } + + public func getPostApplicantScreenData(id: String) -> RxSwift.Single { + service.request(api: .getApplicantList(id: id), with: .withToken) + .map(PostApplicantScreenDTO.self) + .map { dto in + dto.toVO() + } + } + + public func closePost(id: String) -> RxSwift.Single { + service.request(api: .closePost(id: id), with: .withToken) + .mapToVoid() + } + + public func removePost(id: String) -> RxSwift.Single { + service.request(api: .removePost(id: id), with: .withToken) + .mapToVoid() + } + // MARK: Worker public func getPostDetailForWorker(id: String) -> RxSwift.Single { service.request( @@ -65,12 +103,12 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { public func getNativePostListForWorker(nextPageId: String?, requestCnt: Int = 10) -> RxSwift.Single { service.request( - api: .nativePostList(nextPageId: nextPageId, requestCnt: String(requestCnt)), + 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, let response) = moyaError { + if let moyaError = error as? MoyaError, case .objectMapping(let error, _) = moyaError { print(error.localizedDescription) } return .error(error) @@ -81,7 +119,8 @@ public class DefaultRecruitmentPostRepository: RecruitmentPostRepository { } } -fileprivate extension RegisterRecruitmentPostBundle { +// MARK: 공고등록 정보를 DTO로 변환하는 영역 +extension RegisterRecruitmentPostBundle { func toDTO() -> RecruitmentPostRegisterDTO { @@ -161,10 +200,9 @@ fileprivate extension RegisterRecruitmentPostBundle { return dto } } - // MARK: 엔티티 타입들을 DTO로 변경하기 위한 확장 -fileprivate extension WorkDay { +extension WorkDay { var dtoFormString: String { switch self { @@ -186,7 +224,7 @@ fileprivate extension WorkDay { } } -fileprivate extension IdleDateComponent { +extension IdleDateComponent { var dtoFormString: String { if part == .AM { return "\(hour):\(minute)" @@ -196,7 +234,7 @@ fileprivate extension IdleDateComponent { } } -fileprivate extension PaymentType { +extension PaymentType { var dtoFormString: String { switch self { case .hourly: @@ -209,7 +247,7 @@ fileprivate extension PaymentType { } } -fileprivate extension Gender { +extension Gender { var dtoFormString: String { switch self { case .male: @@ -222,13 +260,13 @@ fileprivate extension Gender { } } -fileprivate extension CareGrade { +extension CareGrade { var dtoFormInt: Int { self.rawValue + 1 } } -fileprivate extension CognitionDegree { +extension CognitionDegree { var dtoFormString: String { switch self { case .stable: @@ -241,7 +279,7 @@ fileprivate extension CognitionDegree { } } -fileprivate extension DailySupportType { +extension DailySupportType { var dtoFormString: String { switch self { case .cleaning: @@ -258,7 +296,7 @@ fileprivate extension DailySupportType { } } -fileprivate extension ApplyType { +extension ApplyType { var dtoFormString: String { switch self { case .phoneCall: @@ -271,7 +309,7 @@ fileprivate extension ApplyType { } } -fileprivate extension ApplyDeadlineType { +extension ApplyDeadlineType { var dtoFormString: String { switch self { case .untilApplicationFinished: @@ -282,7 +320,7 @@ fileprivate extension ApplyDeadlineType { } } -fileprivate extension Date { +extension Date { var dtoFormString: String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" @@ -290,3 +328,5 @@ fileprivate extension Date { return dateString } } + + diff --git a/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift b/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift index 3a1169d3..d7ce29ee 100644 --- a/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift +++ b/project/Projects/Data/DataSource/API/RcruitmentPostAPI.swift @@ -12,17 +12,26 @@ import Entity public enum RcruitmentPostAPI { - // Common + /// 요양보호사용 센터용 선택가능 case postDetail(id: String, userType: UserType) // Center + // - 공고 CRUD case registerPost(postData: Data) case editPost(id: String, postData: Data) case removePost(id: String) case closePost(id: String) + // - 공고 상세조회 + case getOnGoingPosts + case getClosedPosts + case getApplicantList(id: String) + + // - 공고 지원자 관련 + case getPostApplicantCount(id: String) + // Worker - case nativePostList(nextPageId: String?, requestCnt: String) + case getOnGoingNativePostListForWorker(nextPageId: String?, requestCnt: String) } extension RcruitmentPostAPI: BaseAPI { @@ -35,6 +44,8 @@ extension RcruitmentPostAPI: BaseAPI { switch self { case .postDetail(let id, let userType): "/\(id)/\(userType.pathUri)" + + case .registerPost: "" case .editPost(let id, _): @@ -43,7 +54,21 @@ extension RcruitmentPostAPI: BaseAPI { "/\(id)" case .closePost(let id): "/\(id)/end" - case .nativePostList: + + + case .getOnGoingPosts: + "/status/in-progress" + case .getClosedPosts: + "/status/completed" + case .getApplicantList(let id): + "/\(id)/applicants" + + + case .getPostApplicantCount(let id): + "/\(id)/applicant-count" + + + case .getOnGoingNativePostListForWorker: "" } } @@ -52,6 +77,8 @@ extension RcruitmentPostAPI: BaseAPI { switch self { case .postDetail: .get + + case .registerPost: .post case .editPost: @@ -60,7 +87,21 @@ extension RcruitmentPostAPI: BaseAPI { .delete case .closePost: .patch - case .nativePostList: + + + case .getOnGoingPosts: + .get + case .getClosedPosts: + .get + case .getApplicantList: + .get + + + case .getPostApplicantCount: + .get + + + case .getOnGoingNativePostListForWorker: .get } } @@ -68,7 +109,7 @@ extension RcruitmentPostAPI: BaseAPI { var bodyParameters: Parameters? { var params: Parameters = [:] switch self { - case .nativePostList(let nextPageId, let requestCnt): + case .getOnGoingNativePostListForWorker(let nextPageId, let requestCnt): if let nextPageId { params["next"] = nextPageId } @@ -81,7 +122,7 @@ extension RcruitmentPostAPI: BaseAPI { var parameterEncoding: ParameterEncoding { switch self { - case .nativePostList: + case .getOnGoingNativePostListForWorker: return URLEncoding.queryString default: return JSONEncoding.default @@ -90,7 +131,7 @@ extension RcruitmentPostAPI: BaseAPI { public var task: Moya.Task { switch self { - case .nativePostList: + case .getOnGoingNativePostListForWorker: .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) case .registerPost(let bodyData): .requestData(bodyData) diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/ApplicantList/PostApplicantDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/ApplicantList/PostApplicantDTO.swift new file mode 100644 index 00000000..034bdc9a --- /dev/null +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/ApplicantList/PostApplicantDTO.swift @@ -0,0 +1,41 @@ +// +// PostApplicantDTO.swift +// DataSource +// +// Created by choijunios on 8/29/24. +// + +import Foundation +import Entity + +public struct PostApplicantDTO: Codable { + public let carerId: String + public let name: String + public let age: Int + public let gender: String + public let experienceYear: Int? + public let profileImageUrl: String? + /// YES + public let jobSearchStatus: String + + public func toVO() -> PostApplicantVO { + + var profileUrl: URL? + if let profileImageUrl { + profileUrl = URL(string: profileImageUrl) + } + + return .init( + workerId: carerId, + profileUrl: profileUrl, + isJobFinding: jobSearchStatus == "YES", + + // MARK: 센터가 즐겨찾기하는 요양보호사 추후 개발예정 + isStared: false, + name: name, + age: age, + gender: Gender.toEntity(text: gender), + expYear: experienceYear + ) + } +} diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/ApplicantList/PostSummaryDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/ApplicantList/PostSummaryDTO.swift new file mode 100644 index 00000000..9f0a2cb2 --- /dev/null +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/ApplicantList/PostSummaryDTO.swift @@ -0,0 +1,55 @@ +// +// PostSummaryDTO.swift +// DataSource +// +// Created by choijunios on 8/29/24. +// + +import Foundation +import Entity + +public struct PostApplicantScreenDTO: Codable { + public let jobPostingSummaryDto: PostSummaryDTO + public let jobPostingApplicants: [PostApplicantDTO] + + public func toVO() -> PostApplicantScreenVO { + + let summaryCardVO = jobPostingSummaryDto.toVO() + let applicantList = jobPostingApplicants.map { $0.toVO() } + + return (summaryCardVO: summaryCardVO, applicantList: applicantList) + } +} + +public struct PostSummaryDTO: Codable { + public let id: String + public let roadNameAddress: String + public let lotNumberAddress: String + public let clientName: String + public let gender: String + public let age: Int + public let careLevel: Int + public let applyDeadlineType: String + public let applyDeadline: String? + public let createdAt: String + + public func toVO() -> CenterEmployCardVO { + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + let startDayDate = dateFormatter.date(from: createdAt)! + let deadLineDate = applyDeadline != nil ? dateFormatter.date(from: applyDeadline!) : nil + + return .init( + postId: id, + startDay: startDayDate, + endDay: deadLineDate, + roadNameAddress: roadNameAddress, + name: clientName, + careGrade: CareGrade(rawValue: careLevel-1)!, + age: age, + gender: Gender.toEntity(text: gender) + ) + } +} diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/PostApplicantCountDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/PostApplicantCountDTO.swift new file mode 100644 index 00000000..3d7d33ac --- /dev/null +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/PostApplicantCountDTO.swift @@ -0,0 +1,12 @@ +// +// PostApplicantCountDTO.swift +// DataSource +// +// Created by choijunios on 8/27/24. +// + +import Foundation + +public struct PostApplicantCountDTO: Decodable { + public let applicantCount: Int +} diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift index 7c17a650..fdd77b1e 100644 --- a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostDTO.swift @@ -1,6 +1,6 @@ // // RecruitmentPostDTO.swift -// ConcreteRepository +// DataSource // // Created by choijunios on 8/8/24. // @@ -132,17 +132,7 @@ public struct RecruitmentPostFetchDTO: Codable { workTimeAndPay.workStartTime = IdleDateComponent.toEntity(text: startTime) workTimeAndPay.workEndTime = IdleDateComponent.toEntity(text: endTime) workTimeAndPay.paymentType = PaymentType.toEntity(text: payType) - - let payAmount = String(payAmount) - var formedPayAmount = "" - for (index, char) in payAmount.reversed().enumerated() { - if (index % 3) == 0, index != 0 { - formedPayAmount += "," - } - formedPayAmount += String(char) - } - - workTimeAndPay.paymentAmount = formedPayAmount + workTimeAndPay.paymentAmount = String(payAmount) let addressInfo: AddressInputStateObject = .init() addressInfo.addressInfo = .init( @@ -156,7 +146,7 @@ public struct RecruitmentPostFetchDTO: Codable { let currentYear = Calendar.current.component(.year, from: Date()) customerInfo.birthYear = String(currentYear - age) - customerInfo.weight = (weight == nil) ? String(weight!) : "" + customerInfo.weight = (weight == nil) ? "" : String(weight!) customerInfo.careGrade = CareGrade(rawValue: careLevel-1)! customerInfo.cognitionState = CognitionDegree.toEntity(text: mentalStatus) diff --git a/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostListForCenterDTO.swift b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostListForCenterDTO.swift new file mode 100644 index 00000000..f186b7b3 --- /dev/null +++ b/project/Projects/Data/DataSource/DTO/RecruitmentPost/RecruitmentPostListForCenterDTO.swift @@ -0,0 +1,47 @@ +// +// RecruitmentPostListForCenterDTO.swift +// DataSource +// +// Created by choijunios on 8/27/24. +// + +import Foundation +import Entity + +public struct RecruitmentPostForCenterListDTO: Decodable { + public let jobPostings: [RecruitmentPostForCenterDTO] +} + +public struct RecruitmentPostForCenterDTO: Decodable { + public let id: String + public let roadNameAddress: String + public let lotNumberAddress: String + public let clientName: String + public let gender: String + public let age: Int + public let careLevel: Int + public let applyDeadlineType: String + public let applyDeadline: String? + public let createdAt: String + + public func toVO() -> RecruitmentPostInfoForCenterVO { + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let applyDeadline = self.applyDeadline != nil ? dateFormatter.date(from: applyDeadline!) : nil + let createdAt = dateFormatter.date(from: self.createdAt)! + + return .init( + id: id, + roadNameAddress: roadNameAddress, + lotNumberAddress: lotNumberAddress, + clientName: clientName, + gender: Gender.toEntity(text: gender), + age: age, + careLevel: CareGrade(rawValue: careLevel-1)!, + applyDeadlineType: ApplyDeadlineType.toEntity(text: applyDeadlineType), + applyDeadline: applyDeadline, + createdAt: createdAt + ) + } +} diff --git a/project/Projects/Data/DataSource/Util/Extension/Single+Extension.swift b/project/Projects/Data/DataSource/Util/Extension/Single+Extension.swift new file mode 100644 index 00000000..98393f93 --- /dev/null +++ b/project/Projects/Data/DataSource/Util/Extension/Single+Extension.swift @@ -0,0 +1,15 @@ +// +// Single+Extension.swift +// ConcreteRepository +// +// Created by choijunios on 8/28/24. +// + +import RxSwift +import Moya + +public extension PrimitiveSequence where Trait == SingleTrait, Element == Response { + func mapToVoid() -> Single { + flatMap { _ in .just(()) } + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift index 295653ae..99fe5d6c 100644 --- a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift @@ -57,6 +57,42 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { convert(task: repository.getPostDetailForWorker(id: id)) } + public func getOngoingPosts() -> RxSwift.Single> { + let task = repository + .getOngoingPosts() + .map { postInfo in + postInfo.forEach { vo in vo.state = .onGoing } + return postInfo + } + return convert(task: task) + } + + public func getClosedPosts() -> RxSwift.Single> { + let task = repository + .getClosedPosts() + .map { postInfo in + postInfo.forEach { vo in vo.state = .closed } + return postInfo + } + return convert(task: task) + } + + public func closePost(id: String) -> Single> { + convert(task: repository.closePost(id: id)) + } + + public func removePost(id: String) -> Single> { + convert(task: repository.removePost(id: id)) + } + + public func getPostApplicantCount(id: String) -> RxSwift.Single> { + convert(task: repository.getPostApplicantCount(id: id)) + } + + public func getPostApplicantScreenData(id: String) -> RxSwift.Single> { + convert(task: repository.getPostApplicantScreenData(id: id)) + } + public func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> { let stream: Single! @@ -68,7 +104,7 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { requestCnt: postCount ) case .thirdParty(let nextPageId): - /// 미구현 + /// 워크넷 가져오기 미구현 fatalError() } diff --git a/project/Projects/Domain/ConcreteUseCaseTests/InputValidationTests.swift b/project/Projects/Domain/ConcreteUseCaseTests/InputValidationTests.swift index d1979a4a..6cbb3daf 100644 --- a/project/Projects/Domain/ConcreteUseCaseTests/InputValidationTests.swift +++ b/project/Projects/Domain/ConcreteUseCaseTests/InputValidationTests.swift @@ -8,7 +8,7 @@ import XCTest @testable import ConcreteUseCase @testable import ConcreteRepository -@testable import NetworkDataSource +@testable import DataSource /// 사용자의 입력을 판단하는 UseCase를 테스트 합니다. final class InputValidationTests: XCTestCase { diff --git a/project/Projects/Domain/Entity/VO/Employ/CenterEmployCardVO.swift b/project/Projects/Domain/Entity/VO/Employ/CenterEmployCardVO.swift index e7aa6f09..03e4ded3 100644 --- a/project/Projects/Domain/Entity/VO/Employ/CenterEmployCardVO.swift +++ b/project/Projects/Domain/Entity/VO/Employ/CenterEmployCardVO.swift @@ -10,54 +10,46 @@ import Foundation public class CenterEmployCardVO { public let postId: String - public let isOngoing: Bool // For rendering - public let startDay: String - public let endDay: String? - public let postTitle: String + public let startDay: Date + public let endDay: Date? + public let roadNameAddress: String public let name: String public let careGrade: CareGrade public let age: Int public let gender: Gender - public let applicantCount: Int public init( - isOngoing: Bool, postId: String, - startDay: String, - endDay: String?, - postTitle: String, + startDay: Date, + endDay: Date?, + roadNameAddress: String, name: String, careGrade: CareGrade, age: Int, - gender: Gender, - applicantCount: Int + gender: Gender ) { - self.isOngoing = isOngoing self.postId = postId self.startDay = startDay self.endDay = endDay - self.postTitle = postTitle + self.roadNameAddress = roadNameAddress self.name = name self.careGrade = careGrade self.age = age self.gender = gender - self.applicantCount = applicantCount } public static var mock: CenterEmployCardVO { .init( - isOngoing: true, postId: "00-00000-00000", - startDay: "12:00", + startDay: Date(), endDay: nil, - postTitle: "서울특별시 강남구 신사동", + roadNameAddress: "서울특별시 강남구 신사동 1231-123", name: "홍길동", careGrade: .one, age: 78, - gender: .female, - applicantCount: 78 + gender: .female ) } } diff --git a/project/Projects/Domain/Entity/VO/Employ/PostApplicantVO.swift b/project/Projects/Domain/Entity/VO/Employ/PostApplicantVO.swift index 89f391a2..83923d8d 100644 --- a/project/Projects/Domain/Entity/VO/Employ/PostApplicantVO.swift +++ b/project/Projects/Domain/Entity/VO/Employ/PostApplicantVO.swift @@ -7,6 +7,8 @@ import Foundation +public typealias PostApplicantScreenVO = (summaryCardVO: CenterEmployCardVO, applicantList: [PostApplicantVO]) + public struct PostApplicantVO { // diff --git a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift b/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift index 08ce2464..658f24cc 100644 --- a/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift +++ b/project/Projects/Domain/Entity/VO/Employ/WorkerEmployCardVO.swift @@ -104,7 +104,7 @@ public struct WorkerEmployCardVO { return WorkerEmployCardVO( dayLeft: leftDay ?? 31, isBeginnerPossible: isBeginnerPossible, - distanceFromWorkPlace: "500m", + distanceFromWorkPlace: "500", title: title, targetAge: targetAge, careGrade: careGrade, diff --git a/project/Projects/Domain/Entity/VO/Post/RecruitmentPostInfoForCenterVO.swift b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostInfoForCenterVO.swift new file mode 100644 index 00000000..d627cf5f --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Post/RecruitmentPostInfoForCenterVO.swift @@ -0,0 +1,43 @@ +// +// RecruitmentPostForCenterVO.swift +// Entity +// +// Created by choijunios on 8/27/24. +// + +import Foundation + +public enum PostState { + case onGoing + case closed +} + +public class RecruitmentPostInfoForCenterVO { + + public let id: String + public let roadNameAddress: String + public let lotNumberAddress: String + public let clientName: String + public let gender: Gender + public let age: Int + public let careLevel: CareGrade + public let applyDeadlineType: ApplyDeadlineType + public let applyDeadline: Date? + public let createdAt: Date + + // MARK: 마감된 공고인지? + public var state: PostState? + + public init(id: String, roadNameAddress: String, lotNumberAddress: String, clientName: String, gender: Gender, age: Int, careLevel: CareGrade, applyDeadlineType: ApplyDeadlineType, applyDeadline: Date?, createdAt: Date) { + self.id = id + self.roadNameAddress = roadNameAddress + self.lotNumberAddress = lotNumberAddress + self.clientName = clientName + self.gender = gender + self.age = age + self.careLevel = careLevel + self.applyDeadlineType = applyDeadlineType + self.applyDeadline = applyDeadline + self.createdAt = createdAt + } +} diff --git a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift index 1e5cf1cf..ff3a9ece 100644 --- a/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/RecruitmentPost/RecruitmentPostRepository.swift @@ -11,7 +11,7 @@ import Entity public protocol RecruitmentPostRepository: RepositoryBase { - // MARK: Center + // MARK: Center - post crud /// 공고를 등록합니다. func registerPost(bundle: RegisterRecruitmentPostBundle) -> Single @@ -21,6 +21,25 @@ public protocol RecruitmentPostRepository: RepositoryBase { /// 센터가 등록한 공고의 상세정보를 수정합니다. func editPostDetail(id: String, bundle: RegisterRecruitmentPostBundle) -> Single + // MARK: Center - check posts + /// 현재 진행중인 공고를 획득합니다. + func getOngoingPosts() -> Single<[RecruitmentPostInfoForCenterVO]> + + /// 현재 진행중인 공고를 획득합니다. + func getClosedPosts() -> Single<[RecruitmentPostInfoForCenterVO]> + + /// 특정 공고의 지원자 수를 확인합니다. + func getPostApplicantCount(id: String) -> Single + + /// 특정 공고의 지원자 리스트를 조회합니다. 요약된 공고정보가 포함되어 있습니다. + func getPostApplicantScreenData(id: String) -> Single + + /// 공고를 종료합니다. + func closePost(id: String) -> Single + + /// 공고를 삭제합니다. + func removePost(id: String) -> Single + // MARK: Worker /// 요양보호사 공고의 상세정보를 조회합니다. func getPostDetailForWorker(id: String) -> Single diff --git a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift index 05d4ae56..917b5571 100644 --- a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift @@ -19,9 +19,26 @@ public protocol RecruitmentPostUseCase: UseCaseBase { /// 센터측이 공고를 수정하는 액션입니다. func editRecruitmentPost(id: String, inputs: RegisterRecruitmentPostBundle) -> Single> - /// 센터측이 공고를 조회하는 액션입니다. + /// 센터측이 공고상세 정보를 조회하는 액션입니다. func getPostDetailForCenter(id: String) -> Single> + /// 진행중인 공고정보를 가져옵니다. + func getOngoingPosts() -> Single> + + /// 지난 공고정보를 가져옵니다. + func getClosedPosts() -> Single> + + /// 공고에 지원한 지원자 수를 가져옵니다. + func getPostApplicantCount(id: String) -> Single> + + /// 지원자 확인 화면에 사용될 정보를 가져옵니다. + func getPostApplicantScreenData(id: String) -> Single> + + /// 공고를 종료합니다. + func closePost(id: String) -> Single> + + /// 공고를 삭제합니다. + func removePost(id: String) -> Single> // MARK: Worker diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/dot3Option.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/dot3Option.imageset/Contents.json new file mode 100644 index 00000000..9e48b8b9 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/dot3Option.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "dot3Option.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/dot3Option.imageset/dot3Option.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/dot3Option.imageset/dot3Option.svg new file mode 100644 index 00000000..d6d518d3 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/dot3Option.imageset/dot3Option.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/trashBox.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/trashBox.imageset/Contents.json new file mode 100644 index 00000000..cb7f078b --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/trashBox.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "trashBox.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/trashBox.imageset/trashBox.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/trashBox.imageset/trashBox.svg new file mode 100644 index 00000000..da2b561d --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/trashBox.imageset/trashBox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift index 1abc7052..7b2eabbf 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift @@ -21,6 +21,42 @@ public protocol IdleAlertViewModelable { var description: String { get } } +public class DefaultIdleAlertVM: IdleAlertViewModelable { + + public let title: String + public let description: String + public let acceptButtonLabelText: String + public let cancelButtonLabelText: String + + public let acceptButtonClicked: RxRelay.PublishRelay = .init() + public let cancelButtonClicked: RxRelay.PublishRelay = .init() + + public let dismiss: RxCocoa.Driver? + + public init( + title: String, + description: String, + acceptButtonLabelText: String, + cancelButtonLabelText: String, + onAccepted: (() -> ())? = nil + ) { + self.title = title + self.description = description + self.acceptButtonLabelText = acceptButtonLabelText + self.cancelButtonLabelText = cancelButtonLabelText + + dismiss = Observable + .merge( + acceptButtonClicked + .map({ _ in onAccepted?() }) + .asObservable(), + cancelButtonClicked.asObservable() + ) + .asDriver(onErrorDriveWith: .never()) + } +} + + public class IdleBigAlertController: UIViewController { let customTranstionDelegate = CustomTransitionDelegate() diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/AuthSelectionButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/AuthSelectionButton.swift index df2ea14b..89f18733 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/AuthSelectionButton.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/AuthSelectionButton.swift @@ -70,12 +70,11 @@ public class AuthSelectionButton: TappableUIView { } private func setObservable() { - self.rx.tap .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] _ in guard let self else { return } - setState(currentState == .accent ? .normal : .accent) + setState(.accent) }) .disposed(by: disposeBag) } diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/BottomSheetButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/BottomSheetButton.swift new file mode 100644 index 00000000..e0fdc542 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/BottomSheetButton.swift @@ -0,0 +1,94 @@ +// +// BottomSheetButton.swift +// DSKit +// +// Created by choijunios on 8/28/24. +// + +import UIKit +import RxCocoa +import RxSwift + +public class BottomSheetButton: TappableUIView { + + // State + public private(set) var isEnabled: Bool = true + + // Init + + // View + let imageView: UIImageView = { + let view = UIImageView() + return view + }() + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle3) + return label + }() + + private let disposeBag = DisposeBag() + + public init(image: UIImage, titleText: String, color: UIColor) { + self.imageView.image = image + self.imageView.tintColor = color + self.titleLabel.textString = titleText + self.titleLabel.attrTextColor = color + + super.init() + + setApearance() + setAutoLayout() + setObservable() + } + + required init?(coder: NSCoder) { fatalError() } + + private func setApearance() { + self.backgroundColor = DSColor.gray0.color + self.layer.setGrayBorder(radius: 8) + } + + private func setAutoLayout() { + + self.layoutMargins = .init(top: 16, left: 16, bottom: 16, right: 16) + + let mainStack = HStack([ + imageView, + titleLabel, + Spacer() + ],spacing: 4, alignment: .center) + imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + self.addSubview(mainStack) + mainStack.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + } + + private func setObservable() { + self.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.alpha = 0.5 + UIView.animate(withDuration: 0.35) { + self?.alpha = 1 + } + }) + .disposed(by: disposeBag) + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + BottomSheetButton( + image: DSIcon.postCheck.image, + titleText: "채용 종료하기", + color: DSColor.red100.color + ) +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift index e637242c..d0e682cb 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift @@ -18,9 +18,9 @@ public struct CenterEmployCardRO { public let careGradeText: String public let ageText: String public let genderText: String - public let applicantCount: Int + public let postState: PostState? - public init(startDay: String, endDay: String, postTitle: String, nameText: String, careGradeText: String, ageText: String, genderText: String, applicantCount: Int) { + public init(startDay: String, endDay: String, postTitle: String, nameText: String, careGradeText: String, ageText: String, genderText: String, postState: PostState? = nil) { self.startDay = startDay self.endDay = endDay self.postTitle = postTitle @@ -28,7 +28,7 @@ public struct CenterEmployCardRO { self.careGradeText = careGradeText self.ageText = ageText self.genderText = genderText - self.applicantCount = applicantCount + self.postState = postState } public static let mock: CenterEmployCardRO = .init( @@ -39,160 +39,57 @@ public struct CenterEmployCardRO { careGradeText: "1등급", ageText: "78세", genderText: "여성", - applicantCount: 2 + postState: .closed ) public static func create(_ vo: CenterEmployCardVO) -> CenterEmployCardRO { - .init( - startDay: vo.startDay, - endDay: vo.endDay ?? "채용 시까지", - postTitle: vo.postTitle, + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let startDayText = dateFormatter.string(from: vo.startDay) + let endDayText = vo.endDay == nil ? "채용 시까지" : dateFormatter.string(from: vo.endDay!) + + var splittedAddress = vo.roadNameAddress.split(separator: " ") + + if splittedAddress.count >= 3 { + splittedAddress = Array(splittedAddress[0..<3]) + } + let addressTitle = splittedAddress.joined(separator: " ") + + return .init( + startDay: startDayText, + endDay: endDayText, + postTitle: addressTitle, nameText: vo.name, careGradeText: "\(vo.careGrade.textForCellBtn)등급", ageText: "\(vo.age)세", - genderText: vo.gender.twoLetterKoreanWord, - applicantCount: vo.applicantCount - ) - } -} - -public protocol CenterEmployCardViewModelable { - - // Output - var renderObject: Driver? { get } - - // Input - var cardClicked: PublishRelay { get } - - // - Buttons - var checkApplicantBtnClicked: PublishRelay { get } - var editPostBtnClicked: PublishRelay { get } - var terminatePostBtnClicked: PublishRelay { get } -} - -public class CenterEmployCard: TappableUIView { - - // Init - - // View - - // Row1, 2, 3 View - let centerEmployCardInfoView = CenterEmployCardInfoView() - - // Row4 - let checkApplicantsButton: IdlePrimaryCardButton = { - let btn = IdlePrimaryCardButton(level: .medium) - btn.label.textString = "" - return btn - }() - - // Row5 - let editPostButton: ImageTextButton = { - let button = ImageTextButton( - iconImage: DSKitAsset.Icons.postEdit.image, - position: .prefix - ) - button.icon.tintColor = DSKitAsset.Colors.gray300.color - button.label.textString = "공고 수정" - button.label.attrTextColor = DSKitAsset.Colors.gray500.color - - return button - }() - let terminatePostButton: ImageTextButton = { - let button = ImageTextButton( - iconImage: DSKitAsset.Icons.postCheck.image, - position: .prefix + genderText: vo.gender.twoLetterKoreanWord ) - button.icon.tintColor = DSKitAsset.Colors.gray300.color - button.label.textString = "채용 종료" - button.label.attrTextColor = DSKitAsset.Colors.gray500.color - - return button - }() - - - // Observable - private let disposeBag = DisposeBag() - - public override init() { - super.init() - - setAppearance() - setLayout() - setObservable() - } - - public required init?(coder: NSCoder) { fatalError() } - - private func setAppearance() { - self.backgroundColor = .white - self.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor - self.layer.borderWidth = 1.0 - self.layer.cornerRadius = 12 } - private func setLayout() { + public static func create(vo: RecruitmentPostInfoForCenterVO) -> CenterEmployCardRO { - self.layoutMargins = .init(top: 16, left: 16, bottom: 16, right: 16) - - let buttonStack = HStack([ - editPostButton, - terminatePostButton, - ], spacing: 4) - - let contentStack = VStack([ - HStack([centerEmployCardInfoView, Spacer()]), - Spacer(height: 12), - checkApplicantsButton, - HStack([buttonStack, Spacer()]) - ], alignment: .fill) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let startDay = dateFormatter.string(from: vo.createdAt) + let endDay = vo.applyDeadline == nil ? "채용 시까지" : dateFormatter.string(from: vo.applyDeadline!) + + var splittedAddress = vo.roadNameAddress.split(separator: " ") - [ - contentStack - ].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - self.addSubview($0) + if splittedAddress.count >= 3 { + splittedAddress = Array(splittedAddress[0..<3]) } + let addressTitle = splittedAddress.joined(separator: " ") - NSLayoutConstraint.activate([ - contentStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), - contentStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor), - contentStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), - contentStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), - ]) - } - - private func setObservable() { } - - public func bind(ro: CenterEmployCardRO) { - - centerEmployCardInfoView.durationLabel.textString = "\(ro.startDay) ~ \(ro.endDay)" - centerEmployCardInfoView.postTitleLabel.textString = ro.postTitle - centerEmployCardInfoView.nameLabel.textString = ro.nameText - centerEmployCardInfoView.informationLabel.textString = "\(ro.careGradeText) \(ro.ageText) \(ro.genderText)" - checkApplicantsButton.label.textString = "지원자 \(ro.applicantCount)명 조회" - } -} - -fileprivate class TextVM: CenterEmployCardViewModelable { - - public let publishObect: PublishRelay = .init() - - var renderObject: RxCocoa.Driver? - - var cardClicked: RxRelay.PublishRelay = .init() - var checkApplicantBtnClicked: RxRelay.PublishRelay = .init() - var editPostBtnClicked: RxRelay.PublishRelay = .init() - var terminatePostBtnClicked: RxRelay.PublishRelay = .init() - - init() { - renderObject = publishObect.asDriver(onErrorJustReturn: .mock) + return .init( + startDay: startDay, + endDay: endDay, + postTitle: addressTitle, + nameText: vo.clientName, + careGradeText: "\(vo.careLevel.textForCellBtn)등급", + ageText: "\(vo.age)세", + genderText: vo.gender.twoLetterKoreanWord, + postState: vo.state + ) } } - -@available(iOS 17.0, *) -#Preview("Preview", traits: .defaultLayout) { - let btn = CenterEmployCard() - btn.bind(ro: .mock) - return btn -} 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 bb6acaf3..6a9dcaae 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 @@ -10,13 +10,67 @@ import RxSwift import RxCocoa import Entity +public protocol CenterEmployCardViewModelable { + + // Output + var renderObject: CenterEmployCardRO { get } + var applicantCountText: Driver? { get } + + // Input + var cardClicked: PublishRelay { get } + + // - Buttons + var checkApplicantBtnClicked: PublishRelay { get } + var editPostBtnClicked: PublishRelay { get } + var terminatePostBtnClicked: PublishRelay { get } +} + public class CenterEmployCardCell: UITableViewCell { var viewModel: CenterEmployCardViewModelable? public static let identifier = String(describing: CenterEmployCardCell.self) - let cardView = CenterEmployCard() + let cardView = CenterEmployCardInfoView() + + // Row4 + let checkApplicantsButton: IdlePrimaryCardButton = { + let btn = IdlePrimaryCardButton(level: .medium) + btn.label.textString = "" + return btn + }() + + // Row5 + lazy var buttonStack: VStack = { + let belowButtonStack = HStack([editPostButton, terminatePostButton,], spacing: 4) + let stack = VStack([ + checkApplicantsButton, + HStack([belowButtonStack, Spacer()]) + ], spacing: 8, alignment: .fill) + return stack + }() + let editPostButton: ImageTextButton = { + let button = ImageTextButton( + iconImage: DSKitAsset.Icons.postEdit.image, + position: .prefix + ) + button.icon.tintColor = DSKitAsset.Colors.gray300.color + button.label.textString = "공고 수정" + button.label.attrTextColor = DSKitAsset.Colors.gray500.color + + return button + }() + let terminatePostButton: ImageTextButton = { + let button = ImageTextButton( + iconImage: DSKitAsset.Icons.postCheck.image, + position: .prefix + ) + button.icon.tintColor = DSKitAsset.Colors.gray300.color + button.label.textString = "채용 종료" + button.label.attrTextColor = DSKitAsset.Colors.gray500.color + + return button + }() private var disposables: [Disposable?]? @@ -40,50 +94,79 @@ public class CenterEmployCardCell: UITableViewCell { contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 8, right: 20)) } - func setAppearance() { } + func setAppearance() { + contentView.backgroundColor = DSColor.gray0.color + contentView.layer.setGrayBorder() + } func setLayout() { + contentView.layoutMargins = .init(top: 16, left: 16, bottom: 16, right: 16) + + let contentStack = VStack([ + cardView, + buttonStack + ], spacing: 12, alignment: .fill) + [ - cardView + contentStack ].forEach { $0.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview($0) } NSLayoutConstraint.activate([ - cardView.topAnchor.constraint(equalTo: contentView.topAnchor), - cardView.leftAnchor.constraint(equalTo: contentView.leftAnchor), - cardView.rightAnchor.constraint(equalTo: contentView.rightAnchor), - cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + 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), ]) } + private func setObservable() { } + public func bind(viewModel: CenterEmployCardViewModelable) { self.viewModel = viewModel + // MARK: 카드 랜더링 + + let ro = viewModel.renderObject + cardView.durationLabel.textString = "\(ro.startDay) ~ \(ro.endDay)" + cardView.postTitleLabel.textString = ro.postTitle + cardView.nameLabel.textString = ro.nameText + cardView.informationLabel.textString = "\(ro.careGradeText) \(ro.ageText) \(ro.genderText)" + + + // MARK: 공고 상태에 따른 카드 버튼 숨김 + buttonStack.isHidden = ro.postState == .closed + + + // MARK: 액션 바인딩 let disposables: [Disposable?] = [ // Output + + // 지원자수 텍스트 viewModel - .renderObject? - .drive(onNext: { [cardView] ro in - cardView.bind(ro: ro) + .applicantCountText? + .drive(onNext: { [weak self] text in + self?.checkApplicantsButton.label.textString = text }), // Input + cardView.rx.tap .bind(to: viewModel.cardClicked), - cardView.checkApplicantsButton + checkApplicantsButton .rx.tap .bind(to: viewModel.checkApplicantBtnClicked), - cardView.editPostButton + editPostButton .rx.tap .bind(to: viewModel.editPostBtnClicked), - cardView.terminatePostButton + terminatePostButton .rx.tap .bind(to: viewModel.terminatePostBtnClicked), ] diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardInfoView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardInfoView.swift index aa095f36..a4c82c78 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardInfoView.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardInfoView.swift @@ -7,7 +7,7 @@ import UIKit -public class CenterEmployCardInfoView: VStack { +public class CenterEmployCardInfoView: TappableUIView { // Row1 let durationLabel: IdleLabel = { let label = IdleLabel(typography: .Body3) @@ -33,14 +33,19 @@ public class CenterEmployCardInfoView: VStack { return label }() - init() { - super.init([], alignment: .leading) + override init() { + super.init() + setAppearance() setLayout() } public required init(coder: NSCoder) { fatalError() } - func setLayout() { + private func setAppearance() { + self.backgroundColor = DSColor.gray0.color + } + + private func setLayout() { // InfoLabel let divider = UIView() @@ -57,14 +62,28 @@ public class CenterEmployCardInfoView: VStack { divider.bottomAnchor.constraint(equalTo: infoStack.bottomAnchor, constant: -5), ]) - [ + let viewList = [ durationLabel, Spacer(height: 4), postTitleLabel, Spacer(height: 2), infoStack, + ] + + let contentStack = VStack(viewList, alignment: .leading) + + [ + HStack([contentStack, Spacer()], alignment: .fill) ].forEach { - self.addArrangedSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) } + + NSLayoutConstraint.activate([ + contentStack.topAnchor.constraint(equalTo: self.topAnchor), + contentStack.leftAnchor.constraint(equalTo: self.leftAnchor), + contentStack.rightAnchor.constraint(equalTo: self.rightAnchor), + contentStack.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ]) } } diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift index 6ae37e2c..ad046f21 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift @@ -16,7 +16,7 @@ public class WorkerEmployCardRO { let showDayLeftTag: Bool let dayLeftTagText: String? let titleText: String - let distanceFromWorkPlace: String + let distanceFromWorkPlaceText: String let targetInfoText: String let workDaysText: String let workTimeText: String @@ -27,7 +27,7 @@ public class WorkerEmployCardRO { self.showDayLeftTag = showDayLeftTag self.dayLeftTagText = dayLeftTagText self.titleText = titleText - self.distanceFromWorkPlace = distanceFromWorkPlace + self.distanceFromWorkPlaceText = distanceFromWorkPlace self.targetInfoText = targetInfoText self.workDaysText = workDaysText self.workTimeText = workTimeText @@ -51,14 +51,30 @@ public class WorkerEmployCardRO { }).map({ $0.korOneLetterText }).joined(separator: ",") let workTimeText = "\(vo.startTime) - \(vo.endTime)" - let payText = "\(vo.paymentType.korLetterText) \(vo.paymentAmount) 원" + + 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: " ") return .init( showBiginnerTag: vo.isBeginnerPossible, showDayLeftTag: showDayLeftTag, dayLeftTagText: dayLeftTagText, - titleText: vo.title, - distanceFromWorkPlace: vo.distanceFromWorkPlace, + titleText: addressTitle, + distanceFromWorkPlace: "\(vo.distanceFromWorkPlace)m", targetInfoText: targetInfoText, workDaysText: workDaysText, workTimeText: workTimeText, @@ -309,7 +325,7 @@ public class WorkerEmployCard: UIView { dayLeftTag.isHidden = !ro.showDayLeftTag dayLeftTag.textString = ro.dayLeftTagText ?? "" titleLabel.textString = ro.titleText - distanceFromWorkPlaceLabel.textString = ro.distanceFromWorkPlace + distanceFromWorkPlaceLabel.textString = ro.distanceFromWorkPlaceText serviceTargetInfoLabel.textString = ro.targetInfoText workDaysLabel.textString = ro.workDaysText workTimeLabel.textString = ro.workTimeText 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 d4f35283..449acd2a 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 @@ -123,9 +123,16 @@ public extension WorkConditionDisplayingView { workTimeLabel.textString = workTimeText let paymentTypeText = object.paymentType?.korLetterText ?? "오류" - let paymentAmountText = object.paymentAmount - workPaymentLabel.textString = "\(paymentTypeText) \(paymentAmountText)원" + let payAmount = String(object.paymentAmount) + var formedPayAmountText = "" + for (index, char) in payAmount.reversed().enumerated() { + if (index % 3) == 0, index != 0 { + formedPayAmountText = "," + formedPayAmountText + } + formedPayAmountText = String(char) + formedPayAmountText + } + workPaymentLabel.textString = "\(paymentTypeText) \(formedPayAmountText)원" } private func applyObject(_ object: AddressInputStateObject) { diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/SelectCSTypeVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/SelectCSTypeVC.swift index 4fc2d84a..8d724135 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/SelectCSTypeVC.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/View/SelectCSTypeVC.swift @@ -12,7 +12,7 @@ import RxSwift import Entity import DSKit -public class SelectCSTypeVC: IdleButtomSheetVC { +public class SelectCSTypeVC: IdleBottomSheetVC { // Init diff --git a/project/Projects/Presentation/DSKit/Sources/Component/BottomSheet/IdleButtomSheetVC.swift b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/BottomSheet/IdleBottomSheetVC.swift similarity index 89% rename from project/Projects/Presentation/DSKit/Sources/Component/BottomSheet/IdleButtomSheetVC.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/ViewController/BottomSheet/IdleBottomSheetVC.swift index f731a1d2..34a8a904 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/BottomSheet/IdleButtomSheetVC.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/BottomSheet/IdleBottomSheetVC.swift @@ -1,8 +1,8 @@ // -// File.swift -// DSKit +// IdleBottomSheetVC.swift +// BaseFeature // -// Created by choijunios on 8/8/24. +// Created by choijunios on 8/28/24. // import UIKit @@ -10,8 +10,9 @@ import RxSwift import RxCocoa import Entity import PresentationCore +import DSKit -open class IdleButtomSheetVC: UIViewController { +open class IdleBottomSheetVC: BaseViewController { // Not init private var gestureBeganPosition: CGPoint = .zero @@ -56,13 +57,11 @@ open class IdleButtomSheetVC: UIViewController { view.backgroundColor = .black.withAlphaComponent(0.0) } - public func setLayout(contentView: UIView) { - - let conerRadius = 16.0 + public func setLayout(contentView: UIView, margin: UIEdgeInsets = .init(top: 0, left: 27, bottom: 80, right: 27)) { sheetView.backgroundColor = .white - sheetView.layer.cornerRadius = conerRadius - sheetView.layoutMargins = .init(top: 0, left: 27, bottom: 64 + conerRadius, right: 27) + sheetView.layer.cornerRadius = 16.0 + sheetView.layoutMargins = margin [ dragSpace, @@ -94,7 +93,7 @@ open class IdleButtomSheetVC: UIViewController { NSLayoutConstraint.activate([ sheetView.rightAnchor.constraint(equalTo: view.rightAnchor), sheetView.leftAnchor.constraint(equalTo: view.leftAnchor), - sheetView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: conerRadius) + sheetView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 16) ]) } @@ -108,7 +107,7 @@ open class IdleButtomSheetVC: UIViewController { } /// 뷰 디스플레이 관련 -extension IdleButtomSheetVC { +extension IdleBottomSheetVC { /// viewDidAppear서브 뷰들의 레이아웃이 결정된 이후 시점(화면상에 나타난 시점)으로, frame, bounds에 근거있는 값들이 할당된 이후이다. public override func viewDidAppear(_ animated: Bool) { @@ -126,7 +125,7 @@ extension IdleButtomSheetVC { } } - func dismissView() { + public func dismissView(onDismissFinished: (() -> ())? = nil) { let height = sheetView.bounds.height sheetView.isUserInteractionEnabled = false @@ -135,13 +134,15 @@ extension IdleButtomSheetVC { sheetView.transform = .init(translationX: 0, y: height) view?.backgroundColor = .black.withAlphaComponent(0.0) } completion: { [weak self] _ in - self?.dismiss(animated: false) + self?.dismiss(animated: false, completion: { + onDismissFinished?() + }) } } } /// 제스처 동작 -extension IdleButtomSheetVC { +extension IdleBottomSheetVC { @objc func onPanGesture(_ gesture: UIPanGestureRecognizer) { diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Picker/OneDayPicker.swift b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/OneDayPicker/OneDayPickerViewController.swift similarity index 98% rename from project/Projects/Presentation/DSKit/Sources/CommonUI/Picker/OneDayPicker.swift rename to project/Projects/Presentation/Feature/Base/Sources/View/ViewController/OneDayPicker/OneDayPickerViewController.swift index 436de930..990311fa 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Picker/OneDayPicker.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/OneDayPicker/OneDayPickerViewController.swift @@ -1,6 +1,6 @@ // // OneDayPicker.swift -// DSKit +// BaseFeature // // Created by choijunios on 8/2/24. // @@ -11,6 +11,7 @@ import RxCocoa import Entity import PresentationCore import FSCalendar +import DSKit public protocol OneDayPickerDelegate: NSObject { @@ -19,7 +20,7 @@ public protocol OneDayPickerDelegate: NSObject { /// 달력뷰를 바텀시트로 표출하는 뷰 입니다. /// setLayout을 반드시 호출해여 합니다. -public class OneDayPickerViewController: IdleButtomSheetVC { +public class OneDayPickerViewController: IdleBottomSheetVC { // View public let calendar = FSCalendar() diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index a90b3163..f4bb042a 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -23,13 +23,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let store = TestStore() try! store.saveAuthToken( - accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMzYyMDAzNywibmJmIjoxNzIzNjIwMDM3LCJleHAiOjE3MjM2MjA2MzcsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkxNGZjMi04YTk4LTdhNDAtYWFmYS04OWM0MDhiZmEyOGMiLCJwaG9uZU51bWJlciI6IjAxMC00NDQ0LTUyMzIiLCJ1c2VyVHlwZSI6ImNlbnRlciJ9.cYk9E0EJwMpX3wxhQq6R5nMKaVGj2yA7csDybB-Jn8o", - refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMjIyNzcxMywibmJmIjoxNzIyMjI3NzEzLCJleHAiOjE3MjM0MzczMTMsInR5cGUiOiJSRUZSRVNIX1RPS0VOIiwidXNlcklkIjoiMDE5MGZjYzUtNThiNS03ZTlmLWExNzUtYWQ1MDI2YzMyODNhIiwidXNlclR5cGUiOiJjZW50ZXIifQ.EtV-qojoAl-H7VVm-Dr2tYf6Hkbx3OdwbsxduAOFf6I" + accessToken: "", + refreshToken: "" ) - let useCase = DefaultCenterProfileUseCase( - repository: DefaultUserProfileRepository(store) - ) +// let useCase = DefaultCenterProfileUseCase( +// repository: DefaultUserProfileRepository(store) +// ) let navigationController = UINavigationController() navigationController.setNavigationBarHidden(true, animated: false) @@ -47,20 +47,20 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // ) // ) // - let vc = CenterSettingVC() - let vm = CenterSettingVM( - coordinator: nil, - settingUseCase: DefaultSettingUseCase(repository: DefaultAuthRepository()), - centerProfileUseCase: DefaultCenterProfileUseCase( - repository: DefaultUserProfileRepository() - ) - ) - - vc.bind(viewModel: vm) - - window = UIWindow(windowScene: windowScene) - window?.rootViewController = vc - window?.makeKeyAndVisible() +// let vc = CenterSettingVC() +// let vm = CenterSettingVM( +// coordinator: nil, +// settingUseCase: DefaultSettingUseCase(repository: DefaultAuthRepository()), +// centerProfileUseCase: DefaultCenterProfileUseCase( +// repository: DefaultUserProfileRepository() +// ) +// ) +// +// vc.bind(viewModel: vm) +// +// window = UIWindow(windowScene: windowScene) +// window?.rootViewController = vc +// window?.makeKeyAndVisible() // coordinator.start() } diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift index c8aff834..5f3b073d 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/Testing.swift @@ -6,7 +6,7 @@ // import Foundation -import NetworkDataSource +import DataSource class TestStore: KeyValueStore { func save(key: String, value: String) throws { diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/CenterRecruitmentPostBoardScreenCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/CenterRecruitmentPostBoardScreenCoordinator.swift index 29439471..8196bd12 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/CenterRecruitmentPostBoardScreenCoordinator.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/CenterRecruitmentPostBoardScreenCoordinator.swift @@ -12,14 +12,25 @@ import Entity public class CenterRecruitmentPostBoardScreenCoordinator: ChildCoordinator { + public struct Dependency { + let navigationController: UINavigationController + let recruitmentPostUseCase: RecruitmentPostUseCase + + public init(navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.navigationController = navigationController + self.recruitmentPostUseCase = recruitmentPostUseCase + } + } + public weak var viewControllerRef: UIViewController? public weak var parent: RecruitmentManagementCoordinatable? + public let navigationController: UINavigationController + let recruitmentPostUseCase: RecruitmentPostUseCase - public init( - navigationController: UINavigationController - ) { - self.navigationController = navigationController + public init(dependency: Dependency) { + self.navigationController = dependency.navigationController + self.recruitmentPostUseCase = dependency.recruitmentPostUseCase } deinit { @@ -28,7 +39,10 @@ public class CenterRecruitmentPostBoardScreenCoordinator: ChildCoordinator { public func start() { let vc = CenterRecruitmentPostBoardVC() - let vm = CenterRecruitmentPostBoardVM(coordinator: self) + let vm = CenterRecruitmentPostBoardVM( + coordinator: self, + recruitmentPostUseCase: recruitmentPostUseCase + ) vc.bind(viewModel: vm) viewControllerRef = vc navigationController.pushViewController(vc, animated: false) @@ -43,8 +57,9 @@ public class CenterRecruitmentPostBoardScreenCoordinator: ChildCoordinator { parent?.showCheckingApplicantScreen(postId: postId) } - public func showPostDetailScreenForCenter(postId: String, applicantCount: Int?) { - parent?.showPostDetailScreenForCenter(postId: postId, applicantCount: applicantCount) + public func showPostDetailScreenForCenter(postId: String, postState: PostState) { + + parent?.showPostDetailScreenForCenter(postId: postId, postState: postState) } public func showEditScreen(postId: String) { diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostDetailForCenterCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostDetailForCenterCoordinator.swift index 848b7c2c..bf3561d1 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostDetailForCenterCoordinator.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/PostDetailForCenterCoordinator.swift @@ -14,13 +14,13 @@ public class PostDetailForCenterCoordinator: ChildCoordinator { public struct Dependency { let postId: String - var applicantCount: Int? + let postState: PostState let navigationController: UINavigationController let recruitmentPostUseCase: RecruitmentPostUseCase - public init(postId: String, applicantCount: Int? = nil, navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase) { + public init(postId: String, postState: PostState, navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase) { self.postId = postId - self.applicantCount = applicantCount + self.postState = postState self.navigationController = navigationController self.recruitmentPostUseCase = recruitmentPostUseCase } @@ -31,7 +31,7 @@ public class PostDetailForCenterCoordinator: ChildCoordinator { public let navigationController: UINavigationController let postId: String - var applicantCount: Int? + let postState: PostState let recruitmentPostUseCase: RecruitmentPostUseCase public init( @@ -39,8 +39,8 @@ public class PostDetailForCenterCoordinator: ChildCoordinator { ) { self.navigationController = dependency.navigationController self.postId = dependency.postId + self.postState = dependency.postState self.recruitmentPostUseCase = dependency.recruitmentPostUseCase - self.applicantCount = dependency.applicantCount } deinit { @@ -50,8 +50,8 @@ public class PostDetailForCenterCoordinator: ChildCoordinator { public func start() { let vc = PostDetailForCenterVC() let vm = PostDetailForCenterVM( - id: postId, - applicantCount: applicantCount, + postId: postId, + postState: postState, coordinator: self, recruitmentPostUseCase: recruitmentPostUseCase ) diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVC.swift index bdb37ff8..ed1a528d 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVC.swift @@ -36,9 +36,9 @@ public class CheckApplicantVC: BaseViewController { // Init // View - let navigationBar: NavigationBarType1 = { - let view = NavigationBarType1(navigationTitle: "지원자 확인") - return view + let navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar(titleText: "지원자 확인") + return bar }() let postSummaryCard: PostInfoCardView = { let view = PostInfoCardView() @@ -121,7 +121,7 @@ public class CheckApplicantVC: BaseViewController { let scrollView = UIScrollView() scrollView.delaysContentTouches = false scrollView.contentInset = .init( - top: 36, + top: 24, left: 0, bottom: 20, right: 0 @@ -150,8 +150,9 @@ public class CheckApplicantVC: BaseViewController { } NSLayoutConstraint.activate([ - navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 21), - navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 12), + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationBar.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + navigationBar.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), scrollView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor), scrollView.leftAnchor.constraint(equalTo: view.leftAnchor), @@ -177,13 +178,24 @@ public class CheckApplicantVC: BaseViewController { self.viewModel = viewModel + // Input navigationBar - .eventPublisher + .backButton.rx.tap .bind(to: viewModel.exitButtonClicked) .disposed(by: disposeBag) - postSummaryCard - .bind(vo: viewModel.postCardVO) + rx.viewWillAppear + .map { _ in } + .bind(to: viewModel.requestpostApplicantVO) + .disposed(by: disposeBag) + + // Output + viewModel + .postCardVO? + .drive(onNext: { [weak self] cardVO in + self?.postSummaryCard.bind(vo: cardVO) + }) + .disposed(by: disposeBag) viewModel .postApplicantVO? @@ -194,10 +206,7 @@ public class CheckApplicantVC: BaseViewController { }) .disposed(by: disposeBag) - rx.viewWillAppear - .map { _ in } - .bind(to: viewModel.requestpostApplicantVO) - .disposed(by: disposeBag) + } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/ClosedPostVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/ClosedPostVC.swift index 031381bb..ad5b3b8f 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/ClosedPostVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/ClosedPostVC.swift @@ -15,10 +15,10 @@ import DSKit public protocol ClosedPostViewModelable { - var closedPostCardVO: Driver<[CenterEmployCardVO]>? { get } + var closedPostInfo: RxCocoa.Driver<[Entity.RecruitmentPostInfoForCenterVO]>? { get } var requestClosedPost: PublishRelay { get } - func createCellVM(vo: CenterEmployCardVO) -> CenterEmployCardViewModelable + func createClosedPostCellVM(postInfo: RecruitmentPostInfoForCenterVO) -> CenterEmployCardViewModelable } public class ClosedPostVC: BaseViewController { @@ -28,19 +28,15 @@ public class ClosedPostVC: BaseViewController { var viewModel: ClosedPostViewModelable? // View - let postTableView: UITableView = { - let tableView = UITableView() - tableView.rowHeight = UITableView.automaticDimension - tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) - return tableView - }() - + let postTableView: UITableView = .init() let tableHeader = BoardSortigHeaderView() + // DataSource + private var postData: [RecruitmentPostInfoForCenterVO] = [] + // Observable private let disposeBag = DisposeBag() - let closedPostCardVO: BehaviorRelay<[CenterEmployCardVO]> = .init(value: []) public init() { super.init(nibName: nil, bundle: nil) @@ -62,6 +58,9 @@ public class ClosedPostVC: BaseViewController { postTableView.separatorStyle = .none postTableView.delaysContentTouches = false + postTableView.rowHeight = UITableView.automaticDimension + postTableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) + postTableView.tableHeaderView = tableHeader tableHeader.frame = .init(origin: .zero, size: .init( @@ -100,10 +99,12 @@ public class ClosedPostVC: BaseViewController { // Output viewModel - .closedPostCardVO? - .drive(onNext: { [weak self] vos in + .closedPostInfo? + .drive(onNext: { [weak self] postInfo in guard let self else { return } - closedPostCardVO.accept(vos) + + self.postData = postInfo + postTableView.reloadData() }) .disposed(by: disposeBag) @@ -119,7 +120,7 @@ public class ClosedPostVC: BaseViewController { extension ClosedPostVC: UITableViewDataSource, UITableViewDelegate { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - closedPostCardVO.value.count + postData.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -127,11 +128,13 @@ extension ClosedPostVC: UITableViewDataSource, UITableViewDelegate { let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell cell.selectionStyle = .none + let data = postData[indexPath.item] + if let viewModel = self.viewModel { - let vo = closedPostCardVO.value[indexPath.row] - let vm = viewModel.createCellVM(vo: vo) + let vm = viewModel.createClosedPostCellVM(postInfo: data) cell.bind(viewModel: vm) } + return cell } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift index 70709db1..03069c5a 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift @@ -15,10 +15,12 @@ import DSKit public protocol OnGoingPostViewModelable { - var ongoingPostCardVO: Driver<[CenterEmployCardVO]>? { get } - var requestOngoingPost: PublishRelay { get } + var ongoingPostInfo: Driver<[RecruitmentPostInfoForCenterVO]>? { get } + var showRemovePostAlert: Driver? { get } - func createCellVM(vo: CenterEmployCardVO) -> CenterEmployCardViewModelable + var requestOngoingPost: PublishRelay { get } + + func createOngoingPostCellVM(postInfo: RecruitmentPostInfoForCenterVO) -> CenterEmployCardViewModelable } public class OnGoingPostVC: BaseViewController { @@ -28,16 +30,11 @@ public class OnGoingPostVC: BaseViewController { var viewModel: OnGoingPostViewModelable? // View - let postTableView: UITableView = { - let tableView = UITableView() - tableView.rowHeight = UITableView.automaticDimension - tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) - return tableView - }() - + let postTableView: UITableView = .init() let tableHeader = BoardSortigHeaderView() - let ongoingPostCardVO: BehaviorRelay<[CenterEmployCardVO]> = .init(value: [.mock]) + // DataSource + private var postData: [RecruitmentPostInfoForCenterVO] = [] // Observable private let disposeBag = DisposeBag() @@ -62,6 +59,9 @@ public class OnGoingPostVC: BaseViewController { postTableView.separatorStyle = .none postTableView.delaysContentTouches = false + postTableView.rowHeight = UITableView.automaticDimension + postTableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) + postTableView.tableHeaderView = tableHeader tableHeader.frame = .init(origin: .zero, size: .init( @@ -101,14 +101,23 @@ public class OnGoingPostVC: BaseViewController { // Output viewModel - .ongoingPostCardVO? - .drive(onNext: { [weak self] vos in + .ongoingPostInfo? + .drive(onNext: { [weak self] postInfo in guard let self else { return } - ongoingPostCardVO.accept(vos) + + self.postData = postInfo + postTableView.reloadData() }) .disposed(by: disposeBag) + viewModel + .showRemovePostAlert? + .drive(onNext: { [weak self] vm in + self?.showIdleModal(viewModel: vm) + }) + .disposed(by: disposeBag) + // Input rx.viewWillAppear .map { _ in } @@ -120,7 +129,7 @@ public class OnGoingPostVC: BaseViewController { extension OnGoingPostVC: UITableViewDataSource, UITableViewDelegate { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - ongoingPostCardVO.value.count + postData.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -128,9 +137,10 @@ extension OnGoingPostVC: UITableViewDataSource, UITableViewDelegate { let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell cell.selectionStyle = .none + let data = postData[indexPath.item] + if let viewModel = self.viewModel { - let vo = ongoingPostCardVO.value[indexPath.row] - let vm = viewModel.createCellVM(vo: vo) + let vm = viewModel.createOngoingPostCellVM(postInfo: data) cell.bind(viewModel: vm) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterBottomSheetVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterBottomSheetVC.swift new file mode 100644 index 00000000..8043b287 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterBottomSheetVC.swift @@ -0,0 +1,211 @@ +// +// PostDetailForCenterBottomSheetVC.swift +// CenterFeature +// +// Created by choijunios on 8/28/24. +// + +import UIKit +import PresentationCore +import BaseFeature +import RxCocoa +import RxSwift +import Entity +import DSKit + +// MARK: 진행중인 공고 +class OngoingPostOptionVC: IdleBottomSheetVC { + + // Init + + // View + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading3) + label.textString = "공고 편집" + label.textAlignment = .center + return label + }() + let editPostButton: BottomSheetButton = .init(image: DSIcon.postEdit.image, titleText: "공고 수정하기", color: DSColor.gray900.color) + private let closePostButton: BottomSheetButton = .init(image: DSIcon.postCheck.image, titleText: "채용 종료하기", color: DSColor.red200.color) + let closePostConfirmed: PublishRelay = .init() + + // Observable + private let disposeBag = DisposeBag() + + public override init() { + super.init() + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + + setLayout() + setObservable() + } + + private func setLayout() { + + let viewList = [ + titleLabel, + Spacer(height: 20), + editPostButton, + Spacer(height: 8), + closePostButton, + ] + + let mainStack = VStack(viewList, alignment: .fill) + + super.setLayout(contentView: mainStack, margin: .init(top: 0, left: 20, bottom: 48, right: 20)) + } + + private func setObservable() { + + closePostButton + .rx.tap + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + let vm = PostOptionDefaultAlertVM( + title: "채용을 종료하시겠어요?", + description: "채용 종료 시 지원자 정보는 초기화됩니다.", + acceptButtonLabelText: "종료하기", + cancelButtonLabelText: "취소하기" + ) { [weak self] in + self?.dismissView { + // presenter역할을 종료한 이후에 pop합니다. + self?.closePostConfirmed.accept(()) + } + } + + showIdleModal(viewModel: vm) + }) + .disposed(by: disposeBag) + } + + func bind(viewModel: PostDetailViewModelable) { + + editPostButton + .rx.tap + .map({ [weak self] _ in + self?.dismissView() + }) + .bind(to: viewModel.postEditButtonClicked) + .disposed(by: disposeBag) + + closePostConfirmed + .bind(to: viewModel.closePostButtonClicked) + .disposed(by: disposeBag) + } +} + +// MARK: 지난 공고 +class ClosedPostOptionVC: IdleBottomSheetVC { + + // Init + + // View + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading3) + label.textString = "공고 편집" + label.textAlignment = .center + return label + }() + let removePostButton: BottomSheetButton = .init(image: DSIcon.trashBox.image, titleText: "공고 삭제하기", color: DSColor.red200.color) + + let removeConfirmed: PublishRelay = .init() + + // Observable + private let disposeBag = DisposeBag() + + public override init() { + super.init() + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + + setLayout() + setObservable() + } + + private func setLayout() { + + let viewList = [ + titleLabel, + removePostButton + ] + + let mainStack = VStack(viewList, spacing: 20, alignment: .fill) + + super.setLayout(contentView: mainStack, margin: .init(top: 0, left: 20, bottom: 48, right: 20)) + } + + private func setObservable() { + + removePostButton + .rx.tap + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + + let vm = PostOptionDefaultAlertVM( + title: "공고를 삭제하시겠어요?", + description: "삭제 시 공고를 복구할 수 없어요.", + acceptButtonLabelText: "삭제하기", + cancelButtonLabelText: "취소하기" + ) { [weak self] in + self?.dismissView { + self?.removeConfirmed.accept(()) + } + } + + showIdleModal(viewModel: vm) + }) + .disposed(by: disposeBag) + } + + func bind(viewModel: PostDetailViewModelable) { + + removeConfirmed + .bind(to: viewModel.removePostButtonClicked) + .disposed(by: disposeBag) + } +} + +fileprivate class PostOptionDefaultAlertVM: IdleAlertViewModelable { + + var title: String + var description: String + var acceptButtonLabelText: String + var cancelButtonLabelText: String + + var acceptButtonClicked: RxRelay.PublishRelay = .init() + var cancelButtonClicked: RxRelay.PublishRelay = .init() + + var dismiss: RxCocoa.Driver? + + init( + title: String, + description: String, + acceptButtonLabelText: String, + cancelButtonLabelText: String, + onAccept: @escaping () -> () + ) { + self.title = title + self.description = description + self.acceptButtonLabelText = acceptButtonLabelText + self.cancelButtonLabelText = cancelButtonLabelText + + dismiss = Observable + .merge( + acceptButtonClicked.map({ [onAccept] _ in + onAccept() + }), + cancelButtonClicked.asObservable() + ) + .asDriver(onErrorDriveWith: .never()) + } +} 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 5f610202..aa8bfa94 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 @@ -21,22 +21,21 @@ public class PostDetailForCenterVC: BaseViewController { var viewModel: PostDetailViewModelable? // View + let optionButton: UIButton = { + let button = UIButton() + let image = DSIcon.dot3Option.image + button.setImage(image, for: .normal) + button.tintColor = DSColor.gray400.color + return button + }() lazy var navigationBar: IdleNavigationBar = { - let bar = IdleNavigationBar(titleText: "공고 상세 정보", innerViews: [postEditButton]) + let bar = IdleNavigationBar(titleText: "공고 상세 정보", innerViews: [optionButton]) return bar }() - let postEditButton: TextButtonType2 = { - let button = TextButtonType2(labelText: "공고 수정하기") - button.label.typography = .Body3 - button.label.attrTextColor = DSKitAsset.Colors.gray300.color - button.layoutMargins = .init(top: 5.5, left:12, bottom: 5.5, right: 12) - button.layer.cornerRadius = 16 - return button - }() let sampleCard: WorkerEmployCard = { let card = WorkerEmployCard() - card.starButton.isUserInteractionEnabled = false + card.starButton.isHidden = true return card }() @@ -198,7 +197,7 @@ public class PostDetailForCenterVC: BaseViewController { NSLayoutConstraint.activate([ - cardBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor), + cardBackgroundView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), cardBackgroundView.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor), cardBackgroundView.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor), @@ -260,11 +259,18 @@ public class PostDetailForCenterVC: BaseViewController { .bind(to: viewModel.exitButtonClicked) .disposed(by: disposeBag) - // 수정화면으로 이동 - postEditButton.eventPublisher - .bind(to: viewModel.postEditButtonClicked) + // 옵션 버튼 + optionButton + .rx.tap + .bind(to: viewModel.optionButtonClicked) + .disposed(by: disposeBag) + + // 요양보호사가 보는 화면 보기 + screenFoWorkerButton.rx.tap + .bind(to: viewModel.showAsWorkerButtonClicked) .disposed(by: disposeBag) + // 화면이 등장할 때마다 유효한 상태를 불러옵니다. self.rx.viewWillAppear .map({ _ in }) @@ -272,8 +278,35 @@ public class PostDetailForCenterVC: BaseViewController { .disposed(by: disposeBag) // Ouptut + viewModel + .applicantCountText? + .drive(onNext: { [weak self] text in + self?.checkApplicantButton.label.textString = text + }) + .disposed(by: disposeBag) + - checkApplicantButton.label.textString = "지원자 \(viewModel.applicantCount ?? 0)명 조회" + // 옵션 뷰 + viewModel + .showOptionSheet? + .drive(onNext: { [weak self] state in + guard let self, let vm = self.viewModel else { return } + var vc: IdleBottomSheetVC! + switch state { + case .onGoing: + let onGoingVC = OngoingPostOptionVC() + onGoingVC.bind(viewModel: vm) + vc = onGoingVC + case .closed: + let closedVC = ClosedPostOptionVC() + closedVC.bind(viewModel: vm) + vc = closedVC + } + + vc.modalPresentationStyle = .overFullScreen + present(vc, animated: false) + }) + .disposed(by: disposeBag) workConditionOV.bind(viewModel: viewModel) customerInfoOV.bind(viewModel: viewModel) 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 da6b6ead..f36c79bb 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift @@ -9,6 +9,7 @@ import Foundation import UIKit import BaseFeature import PresentationCore +import UseCaseInterface import RxCocoa import RxSwift import Entity @@ -21,66 +22,119 @@ public protocol CenterRecruitmentPostBoardViewModelable: OnGoingPostViewModelabl public class CenterRecruitmentPostBoardVM: CenterRecruitmentPostBoardViewModelable { + // Init weak var coordinator: CenterRecruitmentPostBoardScreenCoordinator? + let recruitmentPostUseCase: RecruitmentPostUseCase public var requestOngoingPost: PublishRelay = .init() public var requestClosedPost: PublishRelay = .init() - public var ongoingPostCardVO: Driver<[CenterEmployCardVO]>? - public var closedPostCardVO: Driver<[CenterEmployCardVO]>? + public var ongoingPostInfo: RxCocoa.Driver<[Entity.RecruitmentPostInfoForCenterVO]>? + public var closedPostInfo: RxCocoa.Driver<[Entity.RecruitmentPostInfoForCenterVO]>? + public var showRemovePostAlert: RxCocoa.Driver? public var alert: Driver? - public init(coordinator: CenterRecruitmentPostBoardScreenCoordinator?) { + let disposeBag = DisposeBag() + + public init(coordinator: CenterRecruitmentPostBoardScreenCoordinator?, recruitmentPostUseCase: RecruitmentPostUseCase) { self.coordinator = coordinator + self.recruitmentPostUseCase = recruitmentPostUseCase let requestOngoingPostResult = requestOngoingPost - .flatMap { [unowned self] _ in - publishOngoingPostMocks() + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase + .getOngoingPosts() } .share() let requestOngoingPostSuccess = requestOngoingPostResult.compactMap { $0.value } let requestOngoingPostFailure = requestOngoingPostResult.compactMap { $0.error } - ongoingPostCardVO = requestOngoingPostSuccess.asDriver(onErrorJustReturn: []) + ongoingPostInfo = requestOngoingPostSuccess + .asDriver(onErrorJustReturn: []) let requestClosedPostResult = requestClosedPost - .flatMap { [unowned self] _ in - publishClosedPostMocks() + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase + .getClosedPosts() } .share() let requestClosedPostSuccess = requestClosedPostResult.compactMap { $0.value } let requestClosedPostFailure = requestClosedPostResult.compactMap { $0.error } - closedPostCardVO = requestClosedPostSuccess.asDriver(onErrorJustReturn: []) + closedPostInfo = requestClosedPostSuccess + .asDriver(onErrorJustReturn: []) + + // MARK: Show + let closePostConfirmed: PublishRelay = .init() + showRemovePostAlert = NotificationCenter.default.rx + .notification(.removePostRequestFromCell) + .map({ notification in + let object = notification.object as! [String: Any] + let postId = object["postId"] as! String + return postId + }) + .map({ (postId: String) in + + let vm = DefaultIdleAlertVM( + title: "채용을 종료하시겠어요?", + description: "채용 종료 시 지원자 정보는 초기화됩니다.", + acceptButtonLabelText: "종료하기", + cancelButtonLabelText: "취소하기" + ) { [closePostConfirmed] in + closePostConfirmed.accept(postId) + } + return vm + }) + .asDriver(onErrorDriveWith: .never()) + + // 채용종료 버튼 + let closePostResult = closePostConfirmed + .flatMap { [recruitmentPostUseCase] postId in + recruitmentPostUseCase.closePost(id: postId) + } + .share() + + let closePostSuccess = closePostResult.compactMap { $0.value } + + // 새로고침 + closePostSuccess + .bind(to: requestOngoingPost) + .disposed(by: disposeBag) + + let closePostFailure = closePostResult.compactMap { $0.error } alert = Observable.merge( requestOngoingPostFailure, - requestClosedPostFailure + requestClosedPostFailure, + closePostFailure ).map { error in DefaultAlertContentVO( - title: "시스템 오류", + title: "메인화면 오류", message: error.message ) } .asDriver(onErrorJustReturn: .default) } - func publishOngoingPostMocks() -> Single> { - return .just(.success((0...10).map { _ in CenterEmployCardVO.mock })) - } - - func publishClosedPostMocks() -> Single> { - return .just(.success((0...10).map { _ in CenterEmployCardVO.mock })) + public func createOngoingPostCellVM(postInfo: Entity.RecruitmentPostInfoForCenterVO) -> any DSKit.CenterEmployCardViewModelable { + CenterEmployCardVM( + postInfo: postInfo, + postState: .onGoing, + coordinator: coordinator, + recruitmentPostUseCase: recruitmentPostUseCase + ) } - public func createCellVM(vo: CenterEmployCardVO) -> any CenterEmployCardViewModelable { + public func createClosedPostCellVM(postInfo: Entity.RecruitmentPostInfoForCenterVO) -> any DSKit.CenterEmployCardViewModelable { CenterEmployCardVM( - vo: vo, - coordinator: coordinator + postInfo: postInfo, + postState: .closed, + coordinator: coordinator, + recruitmentPostUseCase: recruitmentPostUseCase ) } } @@ -88,13 +142,15 @@ public class CenterRecruitmentPostBoardVM: CenterRecruitmentPostBoardViewModelab // MARK: 카드 뷰에 사용될 ViewModel class CenterEmployCardVM: CenterEmployCardViewModelable { - weak var coordinator: CenterRecruitmentPostBoardScreenCoordinator? - // Init - let id: String + let postInfo: RecruitmentPostInfoForCenterVO + let postState: PostState + let recruitmentPostUseCase: RecruitmentPostUseCase + weak var coordinator: CenterRecruitmentPostBoardScreenCoordinator? // Output - var renderObject: Driver? + let renderObject: CenterEmployCardRO + var applicantCountText: Driver? // Input var cardClicked: PublishRelay = .init() @@ -104,38 +160,76 @@ class CenterEmployCardVM: CenterEmployCardViewModelable { let disposeBag = DisposeBag() - init(vo: CenterEmployCardVO, coordinator: CenterRecruitmentPostBoardScreenCoordinator?) { - self.id = vo.postId + init(postInfo: RecruitmentPostInfoForCenterVO, postState: PostState, coordinator: CenterRecruitmentPostBoardScreenCoordinator?, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.postInfo = postInfo + self.postState = postState self.coordinator = coordinator + self.recruitmentPostUseCase = recruitmentPostUseCase // MARK: RenderObject - let publishRelay: BehaviorRelay = .init(value: .mock) - renderObject = publishRelay.asDriver(onErrorJustReturn: .mock) + self.renderObject = CenterEmployCardRO.create(vo: postInfo) + + // MARK: 지원자 수 조회 + let getApplicantCountResult = recruitmentPostUseCase + .getPostApplicantCount(id: postInfo.id) + .asObservable() + .share() + + let getApplicantCountSuccess = getApplicantCountResult.compactMap { $0.value } + let getApplicantCountFailure = getApplicantCountResult.compactMap { $0.error } - publishRelay.accept(CenterEmployCardRO.create(vo)) + applicantCountText = Observable + .merge( + getApplicantCountSuccess.map { cnt in "지원자 \(cnt)명 조회" }.asObservable(), + getApplicantCountFailure.map { error in + printIfDebug("지원자수를 가져올 수 없음 \(error.message)") + return "지원자 수 조회 실패" + }.asObservable() + ) + .asDriver(onErrorDriveWith: .never()) // MARK: 버튼 처리 + cardClicked + .subscribe(onNext: { + [weak self] _ in + guard let self else { return } + + self.coordinator?.showPostDetailScreenForCenter(postId: postInfo.id, postState: postState) + }) + .disposed(by: disposeBag) + + + if postInfo.state == .closed { return } + // 이전 공고인 경우 버튼 바인딩 하지 않음 + checkApplicantBtnClicked .subscribe(onNext: { [weak self] _ in guard let self else { return } - self.coordinator?.showCheckingApplicantScreen(postId: id) + self.coordinator?.showCheckingApplicantScreen(postId: postInfo.id) }) .disposed(by: disposeBag) - cardClicked + + editPostBtnClicked .subscribe(onNext: { [weak self] _ in guard let self else { return } - self.coordinator?.showPostDetailScreenForCenter(postId: id, applicantCount: vo.applicantCount) + self.coordinator?.showEditScreen(postId: postInfo.id) }) .disposed(by: disposeBag) - editPostBtnClicked + + terminatePostBtnClicked .subscribe(onNext: { [weak self] _ in guard let self else { return } - self.coordinator?.showEditScreen(postId: id) + NotificationCenter.default.post( + name: .removePostRequestFromCell, + object: [ + "postId": postInfo.id + ] + ) }) .disposed(by: disposeBag) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CheckApplicantVM.swift similarity index 70% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift rename to project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CheckApplicantVM.swift index 73e916b8..79ccf132 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CheckApplicantVM.swift @@ -11,15 +11,17 @@ import RxCocoa import Entity import DSKit import PresentationCore +import UseCaseInterface public protocol CheckApplicantViewModelable { // Input var requestpostApplicantVO: PublishRelay { get } var exitButtonClicked: PublishRelay { get } + // Output var postApplicantVO: Driver<[PostApplicantVO]>? { get } - var postCardVO: CenterEmployCardVO { get } + var postCardVO: Driver? { get } var alert: Driver? { get } func createApplicantCardVM(vo: PostApplicantVO) -> ApplicantCardVM @@ -27,19 +29,24 @@ public protocol CheckApplicantViewModelable { public class CheckApplicantVM: CheckApplicantViewModelable { + // Init + let postId: String weak var coorindator: CheckApplicantCoordinatable? + let recruitmentPostUseCase: RecruitmentPostUseCase public var exitButtonClicked: PublishRelay = .init() public var requestpostApplicantVO: PublishRelay = .init() - public var postCardVO: CenterEmployCardVO + public var postApplicantVO: Driver<[PostApplicantVO]>? + public var postCardVO: Driver? public var alert: RxCocoa.Driver? let disposeBag = DisposeBag() - public init(postCardVO: CenterEmployCardVO, coorindator: CheckApplicantCoordinatable?) { - self.postCardVO = postCardVO + public init(postId: String, coorindator: CheckApplicantCoordinatable?, recruitmentPostUseCase: RecruitmentPostUseCase) { + self.postId = postId + self.recruitmentPostUseCase = recruitmentPostUseCase self.coorindator = coorindator exitButtonClicked @@ -50,23 +57,30 @@ public class CheckApplicantVM: CheckApplicantViewModelable { .disposed(by: disposeBag) // Input - let requestPostApplicantVOResult = requestpostApplicantVO - .flatMap { [unowned self] _ in - publishPostApplicantVOMocks() + let requestScreenDataResult = requestpostApplicantVO + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase.getPostApplicantScreenData(id: postId) } .share() - let requestPostApplicantSuccess = requestPostApplicantVOResult.compactMap { $0.value } - let requestPostApplicantFailure = requestPostApplicantVOResult.compactMap { $0.error } + let requestScreenDataSuccess = requestScreenDataResult.compactMap { $0.value }.share() + let requestScreenDataFailure = requestScreenDataResult.compactMap { $0.error } // Output - postApplicantVO = requestPostApplicantSuccess.asDriver(onErrorJustReturn: []) + postApplicantVO = requestScreenDataSuccess + .map({ screenData in screenData.applicantList }) + .asDriver(onErrorDriveWith: .never()) + + postCardVO = requestScreenDataSuccess + .map({ screenData in screenData.summaryCardVO }) + .asDriver(onErrorDriveWith: .never()) - alert = requestPostApplicantFailure + + alert = requestScreenDataFailure .map { error in DefaultAlertContentVO( - title: "시스템 오류", + title: "지원자 확인 오류", message: error.message ) } @@ -76,11 +90,6 @@ public class CheckApplicantVM: CheckApplicantViewModelable { public func createApplicantCardVM(vo: PostApplicantVO) -> ApplicantCardVM { .init(vo: vo, coordinator: coorindator) } - - func publishPostApplicantVOMocks() -> Single> { - - .just(.success((0...10).map { _ in PostApplicantVO.mock })) - } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift index cb488146..b5483b8c 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/PostDetailForCenterVM.swift @@ -15,33 +15,66 @@ import BaseFeature public protocol PostDetailViewModelable: AnyObject, - PostDetailDisplayingViewModelable + PostDetailDisplayingViewModelable, DefaultAlertOutputable { - + // Output + var applicantCountText: Driver? { get } var workerEmployCardVO: Driver? { get } var requestDetailFailure: Driver? { get } - var applicantCount: Int? { get } + var showOptionSheet: Driver? { get } + // Input - var postEditButtonClicked: PublishRelay { get } + /// 옵션버튼 + var optionButtonClicked: PublishRelay { get } + + /// 나가기 버튼 var exitButtonClicked: PublishRelay { get } + + /// 지원자 확인 var checkApplicationButtonClicked: PublishRelay { get } + + /// 공고 삭제 + var removePostButtonClicked: PublishRelay { get } + + /// 공고 종료 + var closePostButtonClicked: PublishRelay { get } + + /// 요양보호사가 보는 화면으로 보기 + var showAsWorkerButtonClicked: PublishRelay { get } + + /// 공고 수정버튼 + var postEditButtonClicked: PublishRelay { get } + + /// viewWillAppear var viewWillAppear: PublishRelay { get } } public class PostDetailForCenterVM: PostDetailViewModelable { + // Init + let postId: String + let postState: PostState weak var coordinator: PostDetailForCenterCoordinator? + let recruitmentPostUseCase: RecruitmentPostUseCase // MARK: DetailVC Interaction public var applicantCount: Int? public var workerEmployCardVO: RxCocoa.Driver? public var requestDetailFailure: RxCocoa.Driver? + public var showOptionSheet: RxCocoa.Driver? + public var alert: RxCocoa.Driver? + + public let postEditButtonClicked: RxRelay.PublishRelay = .init() + public let exitButtonClicked: RxRelay.PublishRelay = .init() + public let checkApplicationButtonClicked: RxRelay.PublishRelay = .init() + public let optionButtonClicked: RxRelay.PublishRelay = .init() + public let removePostButtonClicked: RxRelay.PublishRelay = .init() + public let closePostButtonClicked: RxRelay.PublishRelay = .init() + public let showAsWorkerButtonClicked: RxRelay.PublishRelay = .init() + + public let viewWillAppear: RxRelay.PublishRelay = .init() - public var postEditButtonClicked: RxRelay.PublishRelay = .init() - public var exitButtonClicked: RxRelay.PublishRelay = .init() - public var checkApplicationButtonClicked: RxRelay.PublishRelay = .init() - public var viewWillAppear: RxRelay.PublishRelay = .init() // MARK: fetched private let fetched_workTimeAndPay: BehaviorRelay = .init(value: .init()) @@ -68,18 +101,22 @@ public class PostDetailForCenterVM: PostDetailViewModelable { return dict }() + // MARK: ETC + public let applicantCountText: Driver? + let disposeBag = DisposeBag() init( - id: String, - applicantCount: Int?, + postId: String, + postState: PostState, coordinator: PostDetailForCenterCoordinator?, recruitmentPostUseCase: RecruitmentPostUseCase ) { - + self.postId = postId + self.postState = postState self.coordinator = coordinator - self.applicantCount = applicantCount + self.recruitmentPostUseCase = recruitmentPostUseCase casting_workTimeAndPay = fetched_workTimeAndPay.asDriver() casting_customerRequirement = fetched_customerRequirement.asDriver() @@ -87,15 +124,30 @@ public class PostDetailForCenterVM: PostDetailViewModelable { casting_applicationDetail = fetched_applicationDetail.asDriver() casting_addressInput = fetched_addressInfo.asDriver() - // MARK: Post card + // MARK: 지원자 수 조회 + let getApplicantCountResult = recruitmentPostUseCase + .getPostApplicantCount(id: postId) + .asObservable() + .share() + let getApplicantCountSuccess = getApplicantCountResult.compactMap { $0.value } + let getApplicantCountFailure = getApplicantCountResult.compactMap { $0.error } + applicantCountText = Observable + .merge( + getApplicantCountSuccess.map { cnt in "지원자 \(cnt)명 조회" }.asObservable(), + getApplicantCountFailure.map { error in + printIfDebug("지원자수를 가져올 수 없음 \(error.message)") + return "지원자 수 조회 실패" + }.asObservable() + ) + .asDriver(onErrorDriveWith: .never()) // MARK: Detail View let fetchPostDetailResult = viewWillAppear .flatMap { [recruitmentPostUseCase] _ in recruitmentPostUseCase - .getPostDetailForCenter(id: id) + .getPostDetailForCenter(id: postId) } .share() @@ -111,6 +163,8 @@ public class PostDetailForCenterVM: PostDetailViewModelable { }) .asDriver(onErrorJustReturn: .default) + + // MARK: 요양보호사 버전 카드뷰 workerEmployCardVO = fetchPostDetailSuccess .map { [weak self] bundle in guard let self else { return .default } @@ -132,21 +186,80 @@ public class PostDetailForCenterVM: PostDetailViewModelable { .asDriver(onErrorJustReturn: .default) + // MARK: 버튼 처리 + + // 옵션스크린 표출 + showOptionSheet = optionButtonClicked + .map { _ in postState } + .asDriver(onErrorDriveWith: .never()) + + // 공고 수정 버튼 postEditButtonClicked .subscribe(onNext: { [weak self] _ in - self?.coordinator?.showPostEditScreen(postId: id) + self?.coordinator?.showPostEditScreen(postId: postId) + }) + .disposed(by: disposeBag) + + // 채용종료 버튼 + let closePostResult = closePostButtonClicked + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase.closePost(id: postId) + } + .share() + + let closePostSuccess = closePostResult.compactMap { $0.value } + let closePostFailure = closePostResult.compactMap { $0.error } + + + // 공고삭제 버튼 + let removePostResult = removePostButtonClicked + .flatMap { [recruitmentPostUseCase] _ in + recruitmentPostUseCase.removePost(id: postId) + } + .share() + + let removePostSuccess = removePostResult.compactMap { $0.value } + let removePostFailure = removePostResult.compactMap { $0.error } + + Observable + .merge(closePostSuccess, removePostSuccess) + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + + self.alert = Observable + .merge(removePostFailure, closePostFailure) + .map { error in + DefaultAlertContentVO( + title: "공고 상세화면 오류", + message: error.message + ) + } + .asDriver(onErrorJustReturn: .default) + + + // 요양보호사 화면 보기 버튼 + showAsWorkerButtonClicked + .subscribe(onNext: { [weak self] _ in + + // 코디네이터 이용 + }) .disposed(by: disposeBag) + // 나가기 버튼 exitButtonClicked .subscribe(onNext: { [weak self] _ in self?.coordinator?.coordinatorDidFinish() }) .disposed(by: disposeBag) + // 지원자확인 버튼 checkApplicationButtonClicked .subscribe(onNext: { [weak self] _ in - self?.coordinator?.showCheckApplicantScreen(postId: id) + self?.coordinator?.showCheckApplicantScreen(postId: postId) }) .disposed(by: disposeBag) } diff --git a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift index 34f74e35..3d2aa977 100644 --- a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift @@ -9,7 +9,6 @@ import UIKit import RootFeature import ConcreteUseCase import ConcreteRepository -import NetworkDataSource class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -20,40 +19,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } - - let store = TestStore() - - try! store.saveAuthToken( - accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMzYyMTQ5MiwibmJmIjoxNzIzNjIxNDkyLCJleHAiOjE3MjM2MjIwOTIsInR5cGUiOiJBQ0NFU1NfVE9LRU4iLCJ1c2VySWQiOiIwMTkxNGZjMi04YTk4LTdhNDAtYWFmYS04OWM0MDhiZmEyOGMiLCJwaG9uZU51bWJlciI6IjAxMC00NDQ0LTUyMzIiLCJ1c2VyVHlwZSI6ImNlbnRlciJ9.WVD8-17nNTewK1EAARw_s-rxfs-6n1pZTyqCdvseIW8", - refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsInN1YiI6bnVsbCwiaXNzIjoiM2lkaW90cyIsImlhdCI6MTcyMzYyMDAzNywibmJmIjoxNzIzNjIwMDM3LCJleHAiOjE3MjQ4Mjk2MzcsInR5cGUiOiJSRUZSRVNIX1RPS0VOIiwidXNlcklkIjoiMDE5MTRmYzItOGE5OC03YTQwLWFhZmEtODljNDA4YmZhMjhjIiwidXNlclR5cGUiOiJjZW50ZXIifQ.hlnjMjEGDD11_XAR2QlfiT1awQoccvE04aqhkZUmWTc" - ) - - let nav = UINavigationController() - nav.setNavigationBarHidden(true, animated: false) - + window = UIWindow(windowScene: windowScene) - window?.rootViewController = nav window?.makeKeyAndVisible() - coordinator.start() } } - -class TestStore: KeyValueStore { - func save(key: String, value: String) throws { - UserDefaults.standard.setValue(value, forKey: key) - } - - func get(key: String) -> String? { - UserDefaults.standard.string(forKey: key) - } - - func delete(key: String) throws { - - } - - func removeAll() throws { - - } - -} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CheckApplicantCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CheckApplicantCoordinator.swift index 9f99a774..24ebe718 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CheckApplicantCoordinator.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CheckApplicantCoordinator.swift @@ -18,13 +18,15 @@ public class CheckApplicantCoordinator: CheckApplicantCoordinatable { public var childCoordinators: [any PresentationCore.Coordinator] = [] public struct Dependency { + let postId: String let navigationController: UINavigationController - let centerEmployCardVO: CenterEmployCardVO + let recruitmentPostUseCase: RecruitmentPostUseCase let workerProfileUseCase: WorkerProfileUseCase - public init(navigationController: UINavigationController, centerEmployCardVO: CenterEmployCardVO, workerProfileUseCase: WorkerProfileUseCase) { + public init(postId: String, navigationController: UINavigationController, recruitmentPostUseCase: RecruitmentPostUseCase, workerProfileUseCase: WorkerProfileUseCase) { + self.postId = postId self.navigationController = navigationController - self.centerEmployCardVO = centerEmployCardVO + self.recruitmentPostUseCase = recruitmentPostUseCase self.workerProfileUseCase = workerProfileUseCase } } @@ -32,16 +34,18 @@ public class CheckApplicantCoordinator: CheckApplicantCoordinatable { public weak var viewControllerRef: UIViewController? public weak var parent: ParentCoordinator? + let postId: String public let navigationController: UINavigationController - let centerEmployCardVO: CenterEmployCardVO let workerProfileUseCase: WorkerProfileUseCase + let recruitmentPostUseCase: RecruitmentPostUseCase public init( dependency: Dependency ) { + self.postId = dependency.postId self.navigationController = dependency.navigationController - self.centerEmployCardVO = dependency.centerEmployCardVO self.workerProfileUseCase = dependency.workerProfileUseCase + self.recruitmentPostUseCase = dependency.recruitmentPostUseCase } deinit { @@ -51,8 +55,9 @@ public class CheckApplicantCoordinator: CheckApplicantCoordinatable { public func start() { let vc = CheckApplicantVC() let vm = CheckApplicantVM( - postCardVO: centerEmployCardVO, - coorindator: self + postId: postId, + coorindator: self, + recruitmentPostUseCase: recruitmentPostUseCase ) vc.bind(viewModel: vm) viewControllerRef = vc diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CenterDeregisterReasonsVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/ViewModel/CenterDeregisterReasonsVM.swift similarity index 100% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CenterDeregisterReasonsVM.swift rename to project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/ViewModel/CenterDeregisterReasonsVM.swift diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVC.swift similarity index 100% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVC.swift rename to project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVC.swift 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 new file mode 100644 index 00000000..a5bcceaa --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift @@ -0,0 +1,165 @@ +// +// InitialScreenVM.swift +// RootFeature +// +// Created by choijunios on 8/25/24. +// + +import RxSwift +import RxCocoa +import Foundation +import PresentationCore +import UseCaseInterface +import RepositoryInterface + +public class InitialScreenVM { + + weak var coordinator: RootCoorinatable? + + // Input + let viewWillAppear: PublishRelay = .init() + + let workerProfileUseCase: WorkerProfileUseCase + let centerProfileUseCase: CenterProfileUseCase + let userInfoLocalRepository: UserInfoLocalRepository + + let disposeBag = DisposeBag() + + public init( + coordinator: RootCoorinatable?, + workerProfileUseCase: WorkerProfileUseCase, + centerProfileUseCase: CenterProfileUseCase, + userInfoLocalRepository: UserInfoLocalRepository + ) + { + self.coordinator = coordinator + self.workerProfileUseCase = workerProfileUseCase + self.centerProfileUseCase = centerProfileUseCase + self.userInfoLocalRepository = userInfoLocalRepository + + // MARK: 로그아웃, 회원탈퇴시 + NotificationCenter.default.rx.notification(.popToInitialVC) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + + guard let self else { return } + + self.coordinator?.popToRoot() + }) + .disposed(by: disposeBag) + + + viewWillAppear + .subscribe(onNext: { [weak self] _ in + + self?.userCheckingFlow() + }) + .disposed(by: disposeBag) + } + + private func userCheckingFlow() { + + // 유저 타입확인 + guard let userType = userInfoLocalRepository.getUserType() else { + printIfDebug("☑️ 저장된 유저정보가 없습니다 Auth화면으로 이동합니다.") + coordinator?.auth() + return + } + + // 로컬 유저정보확인 + if userType == .center { + + // 센터관리자 확인 + printIfDebug("☑️ 센터관리자 정보를 확인합니다.") + + // 저장된 센터정보가 없는 경우 + let requestCenterInfoResult = centerProfileUseCase + .getProfile(mode: .myProfile) + .asObservable() + .share() + let success = requestCenterInfoResult.compactMap { $0.value } + let failure = requestCenterInfoResult.compactMap { $0.error } + + success + .subscribe(onNext: { [weak self] fetchedVO in + + guard let self else { return } + + userInfoLocalRepository.updateCurrentCenterData(vo: fetchedVO) + + printIfDebug("✅ 센터관리자 프로필 정보가 존재합니다.") + + // 센터 메인화면으로 이동 + coordinator?.centerMain() + }) + .disposed(by: disposeBag) + + // 실패한 경우 + failure + .subscribe(onNext: { [weak self] error in + + guard let self else { return } + + if error == .tokenExpiredException { + // 토큰이 만료된 경우로 재로그인 필요 + + printIfDebug("☑️ 재로그인이 필요합니다.") + + coordinator?.auth() + return + } + + printIfDebug("☑️ 센터관리자 프로필 정보가 없습니다.") + + // 센터 메인화면으로 이동 + coordinator?.centerMain() + }) + .disposed(by: disposeBag) + } else { + + // 요양보호사 확인 + let requestWorkerInfoResult = workerProfileUseCase + .getProfile(mode: .myProfile) + .asObservable() + .share() + let success = requestWorkerInfoResult.compactMap { $0.value } + let failure = requestWorkerInfoResult.compactMap { $0.error } + + success + .subscribe(onNext: { [weak self] fetchedVO in + + guard let self else { return } + + userInfoLocalRepository.updateCurrentWorkerData(vo: fetchedVO) + + printIfDebug("✅ 요양보호사 프로필 정보가 존재합니다.") + + // 요양보호사 메인화면으로 이동 + coordinator?.workerMain() + }) + .disposed(by: disposeBag) + + // 실패한 경우 + failure + .subscribe(onNext: { [weak self] error in + + guard let self else { return } + + if error == .tokenExpiredException { + // 토큰이 만료된 경우로 재로그인 필요 + + printIfDebug("☑️ 재로그인이 필요합니다.") + + coordinator?.auth() + return + } + + printIfDebug("☑️ 요양보호사 프로필 정보가 없습니다.") + + // 요양보호사 메인화면으로 이동 + coordinator?.workerMain() + }) + .disposed(by: disposeBag) + } + } +} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVM.swift deleted file mode 100644 index 14cc5c9e..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVM.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// InitialScreenVM.swift -// RootFeature -// -// Created by choijunios on 8/25/24. -// - -import RxSwift -import RxCocoa -import Foundation -import PresentationCore -import UseCaseInterface -import RepositoryInterface - -public class InitialScreenVM { - - weak var coordinator: RootCoorinatable? - - // Input - let viewWillAppear: PublishRelay = .init() - - let workerProfileUseCase: WorkerProfileUseCase - let centerProfileUseCase: CenterProfileUseCase - let userInfoLocalRepository: UserInfoLocalRepository - - let disposeBag = DisposeBag() - - public init( - coordinator: RootCoorinatable?, - workerProfileUseCase: WorkerProfileUseCase, - centerProfileUseCase: CenterProfileUseCase, - userInfoLocalRepository: UserInfoLocalRepository - ) - { - self.coordinator = coordinator - self.workerProfileUseCase = workerProfileUseCase - self.centerProfileUseCase = centerProfileUseCase - self.userInfoLocalRepository = userInfoLocalRepository - - // MARK: 로그아웃, 회원탈퇴시 - NotificationCenter.default.rx.notification(.popToInitialVC) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] _ in - - guard let self else { return } - - self.coordinator?.popToRoot() - }) - .disposed(by: disposeBag) - - - viewWillAppear - .subscribe(onNext: { [weak self] _ in - - self?.userCheckingFlow() - }) - .disposed(by: disposeBag) - } - - private func userCheckingFlow() { - - // 유저 타입확인 - guard let userType = userInfoLocalRepository.getUserType() else { - printIfDebug("☑️ 저장된 유저정보가 없습니다 Auth화면으로 이동합니다.") - coordinator?.auth() - return - } - - // 로컬 유저정보확인 - if userType == .center { - - // 센터관리자 확인 - printIfDebug("☑️ 센터관리자 정보를 확인합니다.") - - if userInfoLocalRepository.getCurrentCenterData() == nil { - // 저장된 센터정보가 없는 경우 - let requestCenterInfoResult = centerProfileUseCase.getProfile(mode: .myProfile) - let success = requestCenterInfoResult.compactMap { $0.value } - let failure = requestCenterInfoResult.compactMap { $0.error } - - success - .subscribe(onSuccess: { [weak self] fetchedVO in - - guard let self else { return } - - userInfoLocalRepository.updateCurrentCenterData(vo: fetchedVO) - - printIfDebug("✅ 센터관리자 프로필 정보가 존재합니다.") - - // 센터 메인화면으로 이동 - coordinator?.centerMain() - }) - .disposed(by: disposeBag) - - // 실패한 경우 - failure - .subscribe(onSuccess: { [weak self] error in - - guard let self else { return } - - if error == .tokenExpiredException { - // 토큰이 만료된 경우로 재로그인 필요 - - printIfDebug("☑️ 재로그인이 필요합니다.") - - coordinator?.auth() - return - } - - printIfDebug("☑️ 센터관리자 프로필 정보가 없습니다.") - - // 센터 메인화면으로 이동 - coordinator?.centerMain() - }) - .disposed(by: disposeBag) - } else { - coordinator?.centerMain() - } - } else { - - // 요양보호사 확인 - - if userInfoLocalRepository.getCurrentWorkerData() == nil { - // 저장된 요양보호사 - let requestWorkerInfoResult = workerProfileUseCase.getProfile(mode: .myProfile) - let success = requestWorkerInfoResult.compactMap { $0.value } - let failure = requestWorkerInfoResult.compactMap { $0.error } - - success - .subscribe(onSuccess: { [weak self] fetchedVO in - - guard let self else { return } - - userInfoLocalRepository.updateCurrentWorkerData(vo: fetchedVO) - - printIfDebug("✅ 요양보호사 프로필 정보가 존재합니다.") - - // 요양보호사 메인화면으로 이동 - coordinator?.workerMain() - }) - .disposed(by: disposeBag) - - // 실패한 경우 - failure - .subscribe(onSuccess: { [weak self] error in - - guard let self else { return } - - if error == .tokenExpiredException { - // 토큰이 만료된 경우로 재로그인 필요 - - printIfDebug("☑️ 재로그인이 필요합니다.") - - coordinator?.auth() - return - } - - printIfDebug("☑️ 요양보호사 프로필 정보가 없습니다.") - - // 요양보호사 메인화면으로 이동 - coordinator?.workerMain() - }) - .disposed(by: disposeBag) - } else { - coordinator?.workerMain() - } - } - } -} diff --git a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift index d1a21d9b..4178d693 100644 --- a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift @@ -9,7 +9,7 @@ import UIKit import ConcreteUseCase import ConcreteRepository import WorkerFeature -import NetworkDataSource +import DataSource class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -21,29 +21,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window = UIWindow(windowScene: windowScene) - let store = TestStore() - - try! store.saveAuthToken( - accessToken: "", - refreshToken: "" - ) - - let useCase = DefaultWorkerProfileUseCase( - repository: DefaultUserProfileRepository(store) - ) - - let vm = WorkerMyProfileViewModel( - coordinator: nil, - workerProfileUseCase: useCase - ) - - let vc = WorkerProfileViewController() - - vc.bind(vm) - - let nav = UINavigationController(rootViewController: vc) - nav.setNavigationBarHidden(true, animated: false) - window?.rootViewController = WorkerSettingVC() window?.makeKeyAndVisible() } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift index 2d555af9..9b737c16 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift @@ -21,7 +21,6 @@ public protocol Coordinator: AnyObject { public extension Coordinator { func popViewController(animated: Bool = true) { - navigationController.popViewController(animated: animated) } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RecruitmentManagementCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RecruitmentManagementCoordinatable.swift index 22a192c2..fd5de76a 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RecruitmentManagementCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/Center/RecruitmentManagementCoordinatable.swift @@ -10,6 +10,6 @@ import Entity public protocol RecruitmentManagementCoordinatable: ParentCoordinator { func showCheckingApplicantScreen(postId: String) - func showPostDetailScreenForCenter(postId: String, applicantCount: Int?) + func showPostDetailScreenForCenter(postId: String, postState: PostState) func showEditScreen(postId: String) } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Notification/RecruitmentPostNotifications.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Notification/RecruitmentPostNotifications.swift new file mode 100644 index 00000000..b6e0d67e --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Notification/RecruitmentPostNotifications.swift @@ -0,0 +1,13 @@ +// +// RecruitmentPost.swift +// PresentationCore +// +// Created by choijunios on 8/29/24. +// + +import Foundation + +public extension Notification.Name { + + static let removePostRequestFromCell: Notification.Name = .init("removePostRequestFromCell") +}