diff --git a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift index af9f6a96..a75b3866 100644 --- a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift +++ b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift @@ -38,5 +38,12 @@ public struct DomainAssembly: Assembly { repository: repository ) } + + container.register(WorkerProfileUseCase.self) { resolver in + let repository = resolver.resolve(UserProfileRepository.self)! + + return DefaultWorkerProfileUseCase(repository: repository) + } + } } diff --git a/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator+Extension.swift b/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator+Extension.swift index a8beb774..0d965c11 100644 --- a/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator+Extension.swift +++ b/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator+Extension.swift @@ -1,6 +1,6 @@ // // AuthCoordinator+Extension.swift -// AuthFeature +// Idle-iOS // // Created by choijunios on 6/30/24. // diff --git a/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator.swift index 73f4e660..699393c2 100644 --- a/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Auth/AuthCoordinator.swift @@ -1,6 +1,6 @@ // // AuthCoordinator.swift -// AuthFeature +// Idle-iOS // // Created by choijunios on 6/30/24. // @@ -11,6 +11,11 @@ import AuthFeature class AuthCoordinator: ParentCoordinator { + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } + var childCoordinators: [Coordinator] = [] var parent: ParentCoordinator? diff --git a/project/Projects/App/Sources/RootCoordinator/Auth/Center/CenterAuthCoorinator.swift b/project/Projects/App/Sources/RootCoordinator/Auth/Center/CenterAuthCoorinator.swift index 03d40b12..f921e334 100644 --- a/project/Projects/App/Sources/RootCoordinator/Auth/Center/CenterAuthCoorinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Auth/Center/CenterAuthCoorinator.swift @@ -13,6 +13,11 @@ import ConcreteRepository class CenterAuthCoorinator: ParentCoordinator { + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } + var childCoordinators: [Coordinator] = [] var parent: AuthCoordinatable? diff --git a/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift index 8f57a72d..bf39c0f7 100644 --- a/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift @@ -1,6 +1,6 @@ // // WorkerAuthCoordinator.swift -// AuthFeature +// Idle-iOS // // Created by choijunios on 6/30/24. // @@ -12,6 +12,11 @@ import AuthFeature class WorkerAuthCoordinator: ParentCoordinator { + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } + var childCoordinators: [Coordinator] = [] var navigationController: UINavigationController diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift index 6cbad864..f681ed47 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift @@ -9,8 +9,15 @@ import UIKit import DSKit import PresentationCore import RootFeature +import UseCaseInterface class CenterMainCoordinator: CenterMainCoordinatable { + + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } + var childCoordinators: [Coordinator] = [] var parent: ParentCoordinator? @@ -46,7 +53,7 @@ class CenterMainCoordinator: CenterMainCoordinatable { func createNavForTab(tab: CenterMainScreen) -> UINavigationController { let tabNavController = UINavigationController() - tabNavController.setNavigationBarHidden(false, animated: false) + tabNavController.setNavigationBarHidden(true, animated: false) startTabCoordinator( tab: tab, @@ -58,13 +65,16 @@ class CenterMainCoordinator: CenterMainCoordinatable { // #2. 생성한 컨트롤러를 각 탭별 Coordinator에 전달 func startTabCoordinator(tab: CenterMainScreen, navigationController: UINavigationController) { - var coordinator: ChildCoordinator! + var coordinator: Coordinator! switch tab { case .recruitmentManage: coordinator = RecruitmentManagementCoordinator( - parent: self, - navigationController: navigationController + dependency: .init( + parent: self, + navigationController: navigationController, + workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self) + ) ) case .setting: @@ -95,6 +105,7 @@ enum CenterMainScreen: Int, CaseIterable { } } +// Test extension CenterMainCoordinator { /// 센터 정보등록 창을 표시합니다. diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterProfileRegisterCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift similarity index 93% rename from project/Projects/App/Sources/RootCoordinator/Main/Center /CenterProfileRegisterCoordinator.swift rename to project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift index 3729246a..eb2538f4 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterProfileRegisterCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift @@ -12,6 +12,11 @@ import PresentationCore import UseCaseInterface class CenterProfileRegisterCoordinator: CenterProfileRegisterCoordinatable { + + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } var childCoordinators: [Coordinator] = [] diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /RegisterPostCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift similarity index 93% rename from project/Projects/App/Sources/RootCoordinator/Main/Center /RegisterPostCoordinator.swift rename to project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift index e58d5833..ad77cf20 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /RegisterPostCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift @@ -14,6 +14,11 @@ import UseCaseInterface class RegisterRecruitmentPostCoordinator: RegisterRecruitmentPostCoordinatable { + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } + var childCoordinators: [Coordinator] = [] var parent: ParentCoordinator? diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift index a6233de5..1bdea884 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift @@ -11,6 +11,12 @@ import PresentationCore import RootFeature class WorkerMainCoordinator: ParentCoordinator { + + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } + var childCoordinators: [Coordinator] = [] var parent: ParentCoordinator? diff --git a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift index c4fe48ff..e0e9da8e 100644 --- a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift @@ -8,13 +8,13 @@ import UIKit import PresentationCore -struct Dependency { - let navigationController: UINavigationController - let injector: Injector -} - class RootCoordinator: ParentCoordinator { + struct Dependency { + let navigationController: UINavigationController + let injector: Injector + } + var childCoordinators: [Coordinator] = [] let navigationController: UINavigationController diff --git a/project/Projects/Domain/Entity/Error/RecruitmentPost/CenterEmployCardVO.swift b/project/Projects/Domain/Entity/Error/RecruitmentPost/CenterEmployCardVO.swift new file mode 100644 index 00000000..e7aa6f09 --- /dev/null +++ b/project/Projects/Domain/Entity/Error/RecruitmentPost/CenterEmployCardVO.swift @@ -0,0 +1,63 @@ +// +// CenterEmployCardVO.swift +// Entity +// +// Created by choijunios on 8/13/24. +// + +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 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, + name: String, + careGrade: CareGrade, + age: Int, + gender: Gender, + applicantCount: Int + ) { + self.isOngoing = isOngoing + self.postId = postId + self.startDay = startDay + self.endDay = endDay + self.postTitle = postTitle + 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", + endDay: nil, + postTitle: "서울특별시 강남구 신사동", + name: "홍길동", + careGrade: .one, + age: 78, + gender: .female, + applicantCount: 78 + ) + } +} diff --git a/project/Projects/Domain/Entity/VO/Employ/PostApplicantVO.swift b/project/Projects/Domain/Entity/VO/Employ/PostApplicantVO.swift new file mode 100644 index 00000000..89f391a2 --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Employ/PostApplicantVO.swift @@ -0,0 +1,45 @@ +// +// PostApplicantVO.swift +// Entity +// +// Created by choijunios on 8/13/24. +// + +import Foundation + +public struct PostApplicantVO { + + // + public let workerId: String + + // For Render + public let profileUrl: URL? + public let isJobFinding: Bool + public let isStared: Bool + public let name: String + public let age: Int + public let gender: Gender + public let expYear: Int? + + public init(workerId: String, profileUrl: URL?, isJobFinding: Bool, isStared: Bool, name: String, age: Int, gender: Gender, expYear: Int?) { + self.workerId = workerId + self.profileUrl = profileUrl + self.isJobFinding = isJobFinding + self.isStared = isStared + self.name = name + self.age = age + self.gender = gender + self.expYear = expYear + } + + public static let mock: PostApplicantVO = .init( + workerId: "testworkerId", + profileUrl: URL(string: "https://dummyimage.com/600x400/00ffbf/0011ff&text=worker+profile"), + isJobFinding: false, + isStared: false, + name: "홍길동", + age: 51, + gender: .female, + expYear: nil + ) +} diff --git a/project/Projects/Presentation/DSKit/Resources/Colors.xcassets/gray0.colorset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Colors.xcassets/gray0.colorset/Contents.json new file mode 100644 index 00000000..951b9076 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Colors.xcassets/gray0.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/bell.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/bell.imageset/Contents.json new file mode 100644 index 00000000..e3c8da20 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/bell.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bell.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/bell.imageset/bell.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/bell.imageset/bell.svg new file mode 100644 index 00000000..b113db53 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/bell.imageset/bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_check.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_check.imageset/Contents.json new file mode 100644 index 00000000..f3c70e18 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_check.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "post_check.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_check.imageset/post_check.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_check.imageset/post_check.svg new file mode 100644 index 00000000..6f023a6a --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_check.imageset/post_check.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_edit.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_edit.imageset/Contents.json new file mode 100644 index 00000000..88de3996 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_edit.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "post_edit.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_edit.imageset/post_edit.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_edit.imageset/post_edit.svg new file mode 100644 index 00000000..5223407a --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/post_edit.imageset/post_edit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerProfilePlaceholder.imageset/workerProfilePlaceholder.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerProfilePlaceholder.imageset/workerProfilePlaceholder.svg deleted file mode 100644 index 3c7f3e32..00000000 --- a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerProfilePlaceholder.imageset/workerProfilePlaceholder.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerProfilePlaceholder.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/worker_profile_placeholder.imageset/Contents.json similarity index 69% rename from project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerProfilePlaceholder.imageset/Contents.json rename to project/Projects/Presentation/DSKit/Resources/Icons.xcassets/worker_profile_placeholder.imageset/Contents.json index d4ce8ef1..9043bdbb 100644 --- a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/workerProfilePlaceholder.imageset/Contents.json +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/worker_profile_placeholder.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "workerProfilePlaceholder.svg", + "filename" : "worker_profile_placeholder.svg", "idiom" : "universal" } ], diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/worker_profile_placeholder.imageset/worker_profile_placeholder.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/worker_profile_placeholder.imageset/worker_profile_placeholder.svg new file mode 100644 index 00000000..68732a5e --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/worker_profile_placeholder.imageset/worker_profile_placeholder.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/ImagePrefixButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/ImagePrefixButton.swift new file mode 100644 index 00000000..a5647fa8 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/ImagePrefixButton.swift @@ -0,0 +1,103 @@ +// +// ImagePrefixButton.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class ImageTextButton: TappableUIView { + + public enum ImagePose { + case prefix + case postfix + } + + let imagePose: ImagePose + + let icon: UIImageView = { + let icon = UIImageView() + icon.contentMode = .scaleAspectFit + return icon + }() + + public let label: IdleLabel = { + let label = IdleLabel(typography: .Body3) + return label + }() + + private let disposeBag = DisposeBag() + + public init(iconImage: UIImage, position: ImagePose) { + self.imagePose = position + super.init() + + icon.image = iconImage + + setAppearance() + setLayout() + setObservable() + } + + public required init?(coder: NSCoder) { fatalError() } + + private func setAppearance() { + + } + + private func setLayout() { + let mainStack = imagePose == .prefix ? HStack([ + icon, + label + ], spacing: 2, alignment: .center) : HStack([ + label, + icon + ], spacing: 2, alignment: .center) + + NSLayoutConstraint.activate([ + icon.widthAnchor.constraint(equalToConstant: 24), + icon.heightAnchor.constraint(equalTo: icon.widthAnchor), + ]) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(mainStack) + } + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: self.topAnchor), + mainStack.leftAnchor.constraint(equalTo: self.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: self.rightAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ]) + } + + private func setObservable() { + self.rx.tap + .subscribe { [weak self] _ in + self?.alpha = 0.5 + UIView.animate(withDuration: 0.35) { [weak self] in + self?.alpha = 1.0 + } + } + .disposed(by: disposeBag) + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + let btn = ImageTextButton( + iconImage: DSKitAsset.Icons.editPhoto.image, + position: .postfix + ) + btn.label.textString = "공고 수정" + btn.tintColor = .black + return btn +} + 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 new file mode 100644 index 00000000..9a2daceb --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCard.swift @@ -0,0 +1,200 @@ +// +// CenterEmployCard.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import RxSwift +import RxCocoa +import Entity + +public struct CenterEmployCardRO { + public let startDay: String + public let endDay: String + public let postTitle: String + public let nameText: String + public let careGradeText: String + public let ageText: String + public let genderText: String + public let applicantCount: Int + + public init(startDay: String, endDay: String, postTitle: String, nameText: String, careGradeText: String, ageText: String, genderText: String, applicantCount: Int) { + self.startDay = startDay + self.endDay = endDay + self.postTitle = postTitle + self.nameText = nameText + self.careGradeText = careGradeText + self.ageText = ageText + self.genderText = genderText + self.applicantCount = applicantCount + } + + public static let mock: CenterEmployCardRO = .init( + startDay: "2024. 07. 10", + endDay: "2024. 07. 31", + postTitle: "서울특별시 강남구 신사동", + nameText: "홍길동", + careGradeText: "1등급", + ageText: "78세", + genderText: "여성", + applicantCount: 2 + ) + + + public static func create(_ vo: CenterEmployCardVO) -> CenterEmployCardRO { + .init( + startDay: vo.startDay, + endDay: vo.endDay ?? "채용 시까지", + postTitle: vo.postTitle, + 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 + ) + 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() { + + 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) + + [ + contentStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + 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) + } +} + +@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 new file mode 100644 index 00000000..bb6acaf3 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardCell.swift @@ -0,0 +1,93 @@ +// +// CenterEmployCardCell.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import RxSwift +import RxCocoa +import Entity + +public class CenterEmployCardCell: UITableViewCell { + + var viewModel: CenterEmployCardViewModelable? + + public static let identifier = String(describing: CenterEmployCardCell.self) + + let cardView = CenterEmployCard() + + private var disposables: [Disposable?]? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setAppearance() + setLayout() + } + public required init?(coder: NSCoder) { fatalError() } + + public override func prepareForReuse() { + viewModel = nil + + disposables?.forEach { $0?.dispose() } + disposables = nil + } + + public override func layoutSubviews() { + super.layoutSubviews() + + contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 20, bottom: 8, right: 20)) + } + + func setAppearance() { } + + func setLayout() { + + [ + cardView + ].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), + ]) + } + + public func bind(viewModel: CenterEmployCardViewModelable) { + + self.viewModel = viewModel + + let disposables: [Disposable?] = [ + // Output + viewModel + .renderObject? + .drive(onNext: { [cardView] ro in + cardView.bind(ro: ro) + }), + + // Input + cardView.rx.tap + .bind(to: viewModel.cardClicked), + + cardView.checkApplicantsButton + .rx.tap + .bind(to: viewModel.checkApplicantBtnClicked), + + cardView.editPostButton + .rx.tap + .bind(to: viewModel.editPostBtnClicked), + + cardView.terminatePostButton + .rx.tap + .bind(to: viewModel.terminatePostBtnClicked), + ] + + self.disposables = disposables + } +} 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 new file mode 100644 index 00000000..aa095f36 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Center/CenterEmployCardInfoView.swift @@ -0,0 +1,70 @@ +// +// CenterEmployCardInfoView.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit + +public class CenterEmployCardInfoView: VStack { + // Row1 + let durationLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.attrTextColor = DSKitAsset.Colors.gray300.color + return label + }() + + // Row2 + let postTitleLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle2) + return label + }() + + // Row3 + let nameLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.attrTextColor = DSKitAsset.Colors.gray500.color + return label + }() + let informationLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.attrTextColor = DSKitAsset.Colors.gray500.color + return label + }() + + init() { + super.init([], alignment: .leading) + setLayout() + } + + public required init(coder: NSCoder) { fatalError() } + + func setLayout() { + + // InfoLabel + let divider = UIView() + divider.backgroundColor = DSKitAsset.Colors.gray300.color + let infoStack = HStack([ + nameLabel, + divider, + informationLabel, + ], spacing: 8) + + NSLayoutConstraint.activate([ + divider.widthAnchor.constraint(equalToConstant: 1), + divider.topAnchor.constraint(equalTo: infoStack.topAnchor, constant: 5), + divider.bottomAnchor.constraint(equalTo: infoStack.bottomAnchor, constant: -5), + ]) + + [ + durationLabel, + Spacer(height: 4), + postTitleLabel, + Spacer(height: 2), + infoStack, + ].forEach { + self.addArrangedSubview($0) + } + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/ApplyScreen/ApplicantCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/ApplyScreen/ApplicantCard.swift new file mode 100644 index 00000000..f0c0af45 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/ApplyScreen/ApplicantCard.swift @@ -0,0 +1,278 @@ +// +// ApplicantCardView.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import Kingfisher +import Entity + +public struct ApplicantCardRO { + + public let profileUrl: URL? + public let isJobFinding: Bool +// public let isStared: Bool + public let name: String + public let ageText: String + public let genderText: String + public let expText: String + + public init( + profileUrl: URL?, + isJobFinding: Bool, +// isStared: Bool, + name: String, + ageText: String, + genderText: String, + expText: String + ) { + self.profileUrl = profileUrl + self.isJobFinding = isJobFinding +// self.isStared = isStared + self.name = name + self.ageText = ageText + self.genderText = genderText + self.expText = expText + } + + public static let mock: ApplicantCardRO = .init( + profileUrl: URL(string: "https://dummyimage.com/600x400/00ffbf/0011ff&text=worker+profile"), + isJobFinding: false, +// isStared: false, + name: "홍길동", + ageText: "51세", + genderText: "여성", + expText: "1년차" + ) + + public static func create(vo: PostApplicantVO) -> ApplicantCardRO { + .init( + profileUrl: vo.profileUrl, + isJobFinding: vo.isJobFinding, +// isStared: vo.isStared, + name: vo.name, + ageText: "\(vo.age)세", + genderText: vo.gender.twoLetterKoreanWord, + expText: vo.expYear == nil ? "신입" : "\(vo.expYear!)년차" + ) + } +} + +public protocol ApplicantCardViewModelable { + + // - Buttons + var showProfileButtonClicked: PublishRelay { get } + var employButtonClicked: PublishRelay { get } + var staredThisWorker: PublishRelay { get } + + // Output + var renderObject: Driver? { get } +} + +public class ApplicantCard: UIView { + + // View + // Profile + let profileImageContainer: UIImageView = { + + let view = UIImageView() + view.backgroundColor = DSKitAsset.Colors.orange100.color + view.layer.cornerRadius = 36 + view.clipsToBounds = true + view.image = DSKitAsset.Icons.workerProfilePlaceholder.image + view.contentMode = .scaleAspectFit + + return view + }() + let workerProfileImage: UIImageView = { + + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + + return imageView + }() + // Star +// public let starButton: IconWithColorStateButton = { +// let button = IconWithColorStateButton( +// representImage: DSKitAsset.Icons.subscribeStar.image, +// normalColor: DSKitAsset.Colors.gray200.color, +// accentColor: DSKitAsset.Colors.orange300.color +// ) +// return button +// }() + + // Row1 + let workingTag: TagLabel = { + let label = TagLabel( + text: "", + typography: .caption, + textColor: DSKitAsset.Colors.orange500.color, + backgroundColor: DSKitAsset.Colors.orange100.color + ) + return label + }() + + // Row2 + let nameLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle2) + + return label + }() + + // Row3 + let infoLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.attrTextColor = DSKitAsset.Colors.gray500.color + return label + }() + let expLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.attrTextColor = DSKitAsset.Colors.gray500.color + return label + }() + + // 버튼들 + let showProfileButton: IdleSecondaryButton = { + let button = IdleSecondaryButton(level: .medium) + button.label.textString = "프로필 보기" + return button + }() + let employButton: IdlePrimaryButton = { + let button = IdlePrimaryButton(level: .medium) + button.label.textString = "채용하기" + return button + }() + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init(frame: .zero) + 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() { + + self.layoutMargins = .init(top: 12, left: 16, bottom: 12, right: 16) + + profileImageContainer.addSubview(workerProfileImage) + workerProfileImage.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + + profileImageContainer.widthAnchor.constraint(equalToConstant: 72), + profileImageContainer.heightAnchor.constraint(equalTo: profileImageContainer.widthAnchor), + + workerProfileImage.topAnchor.constraint(equalTo: profileImageContainer.topAnchor), + workerProfileImage.leftAnchor.constraint(equalTo: profileImageContainer.leftAnchor), + workerProfileImage.rightAnchor.constraint(equalTo: profileImageContainer.rightAnchor), + workerProfileImage.bottomAnchor.constraint(equalTo: profileImageContainer.bottomAnchor), + ]) + + let nameTitleLabel: IdleLabel = .init(typography: .Body3) + nameTitleLabel.textString = "요양보호사" + + let nameStack = HStack([ + nameLabel, + nameTitleLabel, + ], spacing: 2, alignment: .center) + + let divider = UIView() + divider.backgroundColor = DSKitAsset.Colors.gray300.color + let detailWorkerInfoStack = HStack([ + infoLabel, + divider, + expLabel, + ], spacing: 8) + + NSLayoutConstraint.activate([ + divider.widthAnchor.constraint(equalToConstant: 1), + divider.topAnchor.constraint(equalTo: detailWorkerInfoStack.topAnchor, constant: 3), + divider.bottomAnchor.constraint(equalTo: detailWorkerInfoStack.bottomAnchor, constant: -3), + ]) + + let labelStack = VStack([ + workingTag, + nameStack, + detailWorkerInfoStack, + ], spacing: 2, alignment: .leading) + + let workerInfoStack = HStack([ + profileImageContainer, + labelStack, + Spacer(), +// starButton + ], spacing: 16, alignment: .top) + +// NSLayoutConstraint.activate([ +// starButton.widthAnchor.constraint(equalToConstant: 22), +// starButton.heightAnchor.constraint(equalTo: starButton.widthAnchor), +// ]) + + let buttonStack = HStack([ + showProfileButton, employButton + ], spacing: 8, distribution: .fillEqually) + + let mainStack = VStack([ + workerInfoStack, + buttonStack + ], spacing: 12, alignment: .fill) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor), + mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + } + + private func setObservable() { + + } + + public func bind(ro: ApplicantCardRO) { + + if let imageUrl = ro.profileUrl { + workerProfileImage + .setImage(url: imageUrl) + } + + workingTag.textString = ro.isJobFinding ? "구직중" : "휴식중" +// starButton.setState(ro.isStared ? .accent : .normal) + nameLabel.textString = ro.name + infoLabel.textString = "\(ro.ageText) \(ro.genderText)" + expLabel.textString = "\(ro.expText)" + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + let cardView = ApplicantCard() + cardView.bind(ro: .mock) + return cardView +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/ApplyScreen/ApplicantCardCell.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/ApplyScreen/ApplicantCardCell.swift new file mode 100644 index 00000000..c5af8cb9 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/ApplyScreen/ApplicantCardCell.swift @@ -0,0 +1,93 @@ +// +// ApplicantCardCell.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + + +import UIKit +import RxSwift +import RxCocoa +import Entity + +public class ApplicantCardCell: UITableViewCell { + + public static let identifier = String(describing: ApplicantCardCell.self) + + var viewModel: ApplicantCardViewModelable? + + let cardView = ApplicantCard() + + private var disposables: [Disposable?]? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setAppearance() + setLayout() + } + public required init?(coder: NSCoder) { fatalError() } + + public override func prepareForReuse() { + disposables?.forEach { $0?.dispose() } + disposables = nil + } + + func setAppearance() { } + + public override func layoutSubviews() { + super.layoutSubviews() + + contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: 8, right: 0)) + } + + func setLayout() { + + [ + cardView + ].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), + ]) + } + + public func bind(viewModel: ApplicantCardViewModelable) { + + self.viewModel = viewModel + + let disposables: [Disposable?] = [ + // Output + viewModel + .renderObject? + .drive(onNext: { [cardView] ro in + cardView.bind(ro: ro) + }), + + // Input +// cardView +// .starButton.eventPublisher +// .map { state in +// state == .accent +// } +// .bind(to: viewModel.staredThisWorker), + + cardView + .showProfileButton.rx.tap + .bind(to: viewModel.showProfileButtonClicked), + + cardView + .employButton.rx.tap + .bind(to: viewModel.employButtonClicked), + ] + + self.disposables = disposables + } +} + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/PostInfoCardView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/PostInfoCardView.swift new file mode 100644 index 00000000..5c604efe --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/PostInfoCardView.swift @@ -0,0 +1,87 @@ +// +// WorkerInfoCardView.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import PresentationCore +import RxCocoa +import RxSwift +import Entity + +public class PostInfoCardView: TappableUIView { + + // View + let contentView = CenterEmployCardInfoView() + + // 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() { + self.layoutMargins = .init( + top: 16, + left: 16, + bottom: 16, + right: 16 + ) + + let mainStack = VStack([ + HStack([contentView, Spacer()]) + ],alignment: .fill) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + 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() { } + + public func bind(vo: CenterEmployCardVO) { + let ro = CenterEmployCardRO.create(vo) + contentView.durationLabel.textString = "\(ro.startDay) ~ \(ro.endDay)" + contentView.informationLabel.textString = "\(ro.careGradeText) \(ro.ageText) \(ro.genderText)" + contentView.nameLabel.textString = ro.nameText + contentView.postTitleLabel.textString = ro.postTitle + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + let view = PostInfoCardView() + view.contentView.durationLabel.textString = "2024. 07. 10 ~ 2024. 07. 31" + view.contentView.informationLabel.textString = "1등급 78세 여성" + view.contentView.nameLabel.textString = "홍길동" + view.contentView.postTitleLabel.textString = "서울특별시 강남구 신사동" + + return view +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/WorkerEmployCard.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift similarity index 100% rename from project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/WorkerEmployCard.swift rename to project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCard.swift diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/WorkerEmployCardCell.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift similarity index 96% rename from project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/WorkerEmployCardCell.swift rename to project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift index 93384959..4805f6ed 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/WorkerEmployCardCell.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Post/Worker/WorkerEmployCardCell.swift @@ -12,6 +12,8 @@ import Entity public class WorkerEmployCardCell: UITableViewCell { + public static let identifier = String(describing: WorkerEmployCardCell.self) + let tappableArea: TappableUIView = .init() let cardView = WorkerEmployCard() diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/CenterCard/CenterInfoCardView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift similarity index 100% rename from project/Projects/Presentation/DSKit/Sources/CommonUI/Card/CenterCard/CenterInfoCardView.swift rename to project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleNavigationBar.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleNavigationBar.swift new file mode 100644 index 00000000..73cdefee --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleNavigationBar.swift @@ -0,0 +1,106 @@ +// +// IdleNavigationBar.swift +// DSKit +// +// Created by choijunios on 8/14/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class IdleNavigationBar: UIView { + + // Init parameters + + // View + public let backButton: UIButton = { + + let btn = UIButton() + + let image = DSKitAsset.Icons.back.image + + let imageView = UIImageView(image: image) + btn.setImage(image, for: .normal) + btn.imageView?.contentMode = .scaleAspectFit + + return btn + }() + + public lazy var titleLabel: IdleLabel = { + + let label = IdleLabel(typography: .Subtitle1) + label.textAlignment = .left + return label + }() + + private let disposeBag = DisposeBag() + + public init( + titleText: String = "", + innerViews: [UIView] + ) { + super.init(frame: .zero) + + self.titleLabel.textString = titleText + + setApearance() + setAutoLayout(innerViews: innerViews) + } + + public required init(coder: NSCoder) { fatalError() } + + func setApearance() { + + } + + private func setAutoLayout(innerViews: [UIView]) { + + self.layoutMargins = .init( + top: 20.43, + left: 12, + bottom: 12, + right: 20 + ) + + let mainStack = HStack( + [ + [ + backButton, + Spacer(width: 4), + titleLabel, + Spacer(), + ], + innerViews + ].flatMap { $0 }, + alignment: .center, + distribution: .fill + ) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + backButton.widthAnchor.constraint(equalToConstant: 32), + backButton.heightAnchor.constraint(equalToConstant: 32), + + mainStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + let innerView = Spacer(width: 50, height: 50) + innerView.backgroundColor = .red + let bar = IdleNavigationBar(titleText: "테스트", innerViews: [innerView]) + return bar +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift index b9275cc0..33bcba96 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/TabBar/IdleTabBar.swift @@ -19,6 +19,9 @@ public class IdleTabBar: UIViewController { public private(set) var viewControllers: [UIViewController] = [] private var tabBarItems: [IdleTabBarItem] = [] + // View + var tabBarItemStack: UIView! + // 탭바 아이템 private var tabBarItemViews: [IdleTabBarItemViewable] = [] @@ -71,13 +74,15 @@ public class IdleTabBar: UIViewController { tabBarItemViews = tabBarItems.map { item in TextButtonType1(labelText: item.name) } - + let tabBarItemStack = HStack( tabBarItemViews, alignment: .fill, distribution: .fillEqually ) + self.tabBarItemStack = tabBarItemStack + view.addSubview(tabBarItemStack) tabBarItemStack.translatesAutoresizingMaskIntoConstraints = false @@ -125,7 +130,7 @@ public class IdleTabBar: UIViewController { currentView.topAnchor.constraint(equalTo: view.topAnchor), currentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), currentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - currentView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor) + currentView.bottomAnchor.constraint(equalTo: tabBarItemStack.topAnchor) ]) } diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Button/IdleSecondaryButton.swift b/project/Projects/Presentation/DSKit/Sources/Component/Button/IdleSecondaryButton.swift index 310348b0..1f737799 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/Button/IdleSecondaryButton.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Button/IdleSecondaryButton.swift @@ -30,7 +30,7 @@ public enum IdleSecondaryButtonLevel { var idleTextColor: UIColor { switch self { case .medium: - DSKitAsset.Colors.orange500.color + DSKitAsset.Colors.gray300.color } } @@ -52,14 +52,14 @@ public enum IdleSecondaryButtonLevel { var idleBackgroundColor: UIColor { switch self { case .medium: - .white + DSKitAsset.Colors.gray0.color } } var accentBackgroundColor: UIColor { switch self { case .medium: - DSKitAsset.Colors.orange100.color + DSKitAsset.Colors.gray0.color } } @@ -74,14 +74,14 @@ public enum IdleSecondaryButtonLevel { var idleBorderColor: UIColor { switch self { case .medium: - DSKitAsset.Colors.orange400.color + DSKitAsset.Colors.gray200.color } } var accentBorderColor: UIColor { switch self { case .medium: - DSKitAsset.Colors.orange300.color + DSKitAsset.Colors.gray200.color } } @@ -208,7 +208,7 @@ public class IdleSecondaryButton: TappableUIView { let button = IdleSecondaryButton(level: .medium) button.label.textString = "다음" - button.setEnabled(false) + button.setEnabled(true) return button } diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Button/IdleThirdinaryButton.swift b/project/Projects/Presentation/DSKit/Sources/Component/Button/IdleThirdinaryButton.swift new file mode 100644 index 00000000..0cbb51f1 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/Component/Button/IdleThirdinaryButton.swift @@ -0,0 +1,214 @@ +// +// IdleThirdinaryButton.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public enum IdleThirdinaryButtonLevel { + + case medium + + var buttonHeight: CGFloat { + switch self { + case .medium: + 56 + } + } + + var typography: Typography { + switch self { + case .medium: + .Heading4 + } + } + //textColor + var idleTextColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.gray300.color + } + } + + var accentTextColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.gray300.color + } + } + + var disabledTextColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.gray300.color + } + } + + //background + var idleBackgroundColor: UIColor { + switch self { + case .medium: + .white + } + } + + var accentBackgroundColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.orange100.color + } + } + + var disabledBackgroundColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.gray050.color + } + } + + //border + var idleBorderColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.orange400.color + } + } + + var accentBorderColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.orange300.color + } + } + + var disabledBorderColor: UIColor { + switch self { + case .medium: + DSKitAsset.Colors.gray200.color + } + } +} + +public class IdleThirdinaryButton: TappableUIView { + + // State + public private(set) var isEnabled: Bool = true + + // Init + public let level: IdleSecondaryButtonLevel + + // View + public private(set) lazy var label: IdleLabel = { + + let label = IdleLabel(typography: level.typography) + label.attrTextColor = .white + return label + }() + + public override var intrinsicContentSize: CGSize { + .init(width: super.intrinsicContentSize.width, height: level.buttonHeight) + } + + private let disposeBag = DisposeBag() + + public init( + level: IdleSecondaryButtonLevel + ) { + + self.level = level + super.init() + + setApearance() + setAutoLayout() + setObservable() + } + + required init?(coder: NSCoder) { fatalError() } + + private func setApearance() { + self.layer.cornerRadius = 8 + self.clipsToBounds = true + self.layer.borderWidth = 1.0 + + // InitialSetting + backgroundColor = level.idleBackgroundColor + label.attrTextColor = level.idleTextColor + layer.borderColor = level.idleBorderColor.cgColor + } + + private func setAutoLayout() { + + [ + label + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + label.centerYAnchor.constraint(equalTo: self.centerYAnchor), + label.centerXAnchor.constraint(equalTo: self.centerXAnchor), + ]) + } + + private func setObservable() { + + self.rx.tap + .subscribe { [weak self] _ in + guard let self else { return } + + setToAccent() + + UIView.animate(withDuration: 0.3) { [weak self] in + guard let self else { return } + + setToIdle() + } + } + .disposed(by: disposeBag) + } + + public func setEnabled(_ isEnabled: Bool) { + self.isEnabled = isEnabled + self.isUserInteractionEnabled = isEnabled + + if isEnabled { + setToIdle() + } + else { + setToDisabled() + } + } + + private func setToIdle() { + backgroundColor = level.idleBackgroundColor + label.attrTextColor = level.idleTextColor + layer.borderColor = level.idleBorderColor.cgColor + } + + private func setToAccent() { + backgroundColor = level.accentBackgroundColor + label.attrTextColor = level.accentTextColor + layer.borderColor = level.accentBorderColor.cgColor + } + + private func setToDisabled() { + backgroundColor = level.disabledBackgroundColor + label.attrTextColor = level.disabledTextColor + layer.borderColor = level.disabledBorderColor.cgColor + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + let button = IdleSecondaryButton(level: .medium) + button.label.textString = "다음" + button.setEnabled(false) + return button +} + diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Button/SingleImageButton.swift b/project/Projects/Presentation/DSKit/Sources/Component/Button/SingleImageButton.swift new file mode 100644 index 00000000..30a63df1 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/Component/Button/SingleImageButton.swift @@ -0,0 +1,46 @@ +// +// SingleImageButton.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class SingleImageButton: TappableUIView { + + let imageView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFit + return view + }() + + let disposeBag = DisposeBag() + + public override init() { + + super.init() + + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: self.topAnchor), + imageView.leftAnchor.constraint(equalTo: self.leftAnchor), + imageView.rightAnchor.constraint(equalTo: self.rightAnchor), + imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ]) + + rx.tap + .subscribe { [weak self] _ in + self?.alpha = 0.5 + UIView.animate(withDuration: 0.35) { [weak self] in + self?.alpha = 1.0 + } + } + .disposed(by: disposeBag) + } + + required init?(coder: NSCoder) { + fatalError() + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift b/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift index 1ffea61c..afac6eae 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift @@ -37,4 +37,13 @@ public extension UIImage { view.contentMode = .scaleAspectFit return view } + + /// SingleImageButton을 만듭니다. + func toButton(tintColor: UIColor) -> SingleImageButton { + self.withRenderingMode(.alwaysTemplate) + let btn = SingleImageButton() + btn.tintColor = tintColor + btn.imageView.image = self + return btn + } } diff --git a/project/Projects/Presentation/DSKit/Sources/Component/TabControl/IdleTabControlBar.swift b/project/Projects/Presentation/DSKit/Sources/Component/TabControl/IdleTabControlBar.swift new file mode 100644 index 00000000..a7f3db0a --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/Component/TabControl/IdleTabControlBar.swift @@ -0,0 +1,209 @@ +// +// IdleTabControlBar.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import RxCocoa +import RxSwift + +public protocol IdleTabItem { + associatedtype ID: Equatable + var id: ID { get } + var tabLabelText: String { get } +} + +fileprivate class IdleTabBarCell: TappableUIView { + + let label: IdleLabel = .init(typography: .Subtitle3) + + override init() { + + super.init() + + self.addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + label.centerYAnchor.constraint(equalTo: self.centerYAnchor), + label.centerXAnchor.constraint(equalTo: self.centerXAnchor), + ]) + } + + required init?(coder: NSCoder) { + fatalError() + } +} + +public class IdleTabControlBar: UIView { + + let items: [Item] + + public let statePublisher: BehaviorRelay + + private var buttons: [IdleTabBarCell]! + let movingBar: UIView = { + let view = Spacer(height: 2) + view.backgroundColor = DSKitAsset.Colors.gray900.color + return view + }() + + public private(set) var currentIndex: Int? + + public override var intrinsicContentSize: CGSize { + .init( + width: super.intrinsicContentSize.width, + height: 48 + ) + } + + let disposeBag = DisposeBag() + + public init?(items: [Item], initialItem: Item) { + + if items.isEmpty { return nil } + + self.items = items + self.statePublisher = .init(value: initialItem) + super.init(frame: .zero) + setLayout() + selectItem(item: initialItem, animated: false) + setObservable() + } + public required init?(coder: NSCoder) { fatalError() } + + private func setLayout() { + + buttons = items.map { item in + let btn = IdleTabBarCell() + btn.label.textString = item.tabLabelText + return btn + } + + let buttonStack = HStack(buttons, alignment: .fill, distribution: .fillEqually) + + + let barBackGroundView = Spacer(height: 1) + barBackGroundView.backgroundColor = DSKitAsset.Colors.gray100.color + + [ + buttonStack, + barBackGroundView, + movingBar, + ].enumerated().forEach { index, view in + view.layer.zPosition = CGFloat(index) + view.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(view) + } + + NSLayoutConstraint.activate([ + buttonStack.topAnchor.constraint(equalTo: self.topAnchor), + buttonStack.leftAnchor.constraint(equalTo: self.leftAnchor), + buttonStack.rightAnchor.constraint(equalTo: self.rightAnchor), + buttonStack.bottomAnchor.constraint(equalTo: self.bottomAnchor), + + barBackGroundView.leftAnchor.constraint(equalTo: self.leftAnchor), + barBackGroundView.rightAnchor.constraint(equalTo: self.rightAnchor), + barBackGroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + + movingBar.leftAnchor.constraint(equalTo: self.leftAnchor), + movingBar.bottomAnchor.constraint(equalTo: self.bottomAnchor), + movingBar.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1.0/CGFloat(items.count)) + ]) + } + + private func setObservable() { + + buttons + .enumerated() + .forEach { (index, tappableView) in + tappableView + .rx.tap + .map { [weak self] _ in + self?.items[index] + } + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] item in + self?.selectItem(item: item) + self?.statePublisher.accept(item) + }) + .disposed(by: disposeBag) + } + } + + public func selectItem(item: Item, animated: Bool = true) { + + let moveToIndex = items.firstIndex(where: { $0.id == item.id }) + + if currentIndex == moveToIndex { return } + + UIView.animate(withDuration: animated ? 0.2 : 0) { [weak self] in + + guard let self else { return } + + // 라벨색상변경 + buttons + .enumerated() + .forEach { [weak self] (index, view) in + + let isSelected = item.id == self?.items[index].id + view.label.attrTextColor = isSelected ? DSKitAsset.Colors.gray900.color : DSKitAsset.Colors.gray300.color + } + + // 하단 바이동 + currentIndex = moveToIndex + layoutSubviews() + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + // 레이아웃 요청 이후, 레이아웃 적용 이후 + movingBar.transform = .init(translationX: movingBar.bounds.width * CGFloat(currentIndex ?? 0), y: 0) + } +} + +fileprivate enum TestTab { + case tab1 + case tab2 +} + + +fileprivate struct TestItem: IdleTabItem { + typealias ID = TestTab + var tabLabelText: String + var id: TestTab +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + let item1 = TestItem( + tabLabelText: "진행 중인 공고", + id: .tab1 + ) + let item2 = TestItem( + tabLabelText: "이전 공고", + id: .tab2 + ) + + let tabBar = IdleTabControlBar( + items: [item1, item2], + initialItem: item1 + )! + + let vc = UIViewController() + vc.view.addSubview(tabBar) + tabBar.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + tabBar.leftAnchor.constraint(equalTo: vc.view.leftAnchor), + tabBar.rightAnchor.constraint(equalTo: vc.view.rightAnchor), + tabBar.centerYAnchor.constraint(equalTo: vc.view.centerYAnchor), + ]) + + return vc +} diff --git a/project/Projects/Presentation/DSKit/Sources/UIImageView+KingFisher.swift b/project/Projects/Presentation/DSKit/Sources/UIImageView+KingFisher.swift new file mode 100644 index 00000000..cd2e7327 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/UIImageView+KingFisher.swift @@ -0,0 +1,24 @@ +// +// UIImageView+KingFisher.swift +// DSKit +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import Kingfisher + +public extension UIImageView { + + /// KingFisher를 사용해 이미지를 적용합니다. + /// option + /// - let pngSerializer = FormatIndicatedCacheSerializer.png + func setImage(url: URL) { + let pngSerializer = FormatIndicatedCacheSerializer.png + self + .kf.setImage( + with: url, + options: [.cacheSerializer(pngSerializer)] + ) + } +} diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index c3e1686d..76918688 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -45,14 +45,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { navigationController: navigationController ) - let vc = PostOverviewVC() + let vc = CenterRecruitmentPostBoardVC() - vc.bind(viewModel: vm) +// vc.bind(viewModel: vm) window = UIWindow(windowScene: windowScene) window?.rootViewController = vc window?.makeKeyAndVisible() - coordinator.start() +// coordinator.start() } } 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 new file mode 100644 index 00000000..bdb37ff8 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVC.swift @@ -0,0 +1,221 @@ +// +// CheckApplicantVC.swift +// CenterFeature +// +// Created by choijunios on 8/13/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +class MyTableView: UITableView { + + override var intrinsicContentSize: CGSize { + + return CGSize(width: contentSize.width, height: contentSize.height+self.contentInset.top+self.contentInset.bottom) + } + + override func layoutSubviews() { + super.layoutSubviews() + + invalidateIntrinsicContentSize() + } +} + +public class CheckApplicantVC: BaseViewController { + + typealias Cell = ApplicantCardCell + + var viewModel: CheckApplicantViewModelable? + + // Init + + // View + let navigationBar: NavigationBarType1 = { + let view = NavigationBarType1(navigationTitle: "지원자 확인") + return view + }() + let postSummaryCard: PostInfoCardView = { + let view = PostInfoCardView() + return view + }() + + let applicantTitleLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle2) + label.textString = "위 공고에 지원한 보호사 목록이에요." + return label + }() + + let applicantTableView: MyTableView = { + let tableView = MyTableView() + return tableView + }() + + // Observable + private let disposeBag = DisposeBag() + + let postApplicantVO: BehaviorRelay<[PostApplicantVO]> = .init(value: []) + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + setAppearance() + setLayout() + setObservable() + setTableView() + } + + private func setAppearance() { + view.backgroundColor = DSKitAsset.Colors.gray0.color + } + + private func setLayout() { + + let divider = Spacer(height: 8) + divider.backgroundColor = DSKitAsset.Colors.gray050.color + + + let contentView = UIView() + contentView.backgroundColor = DSKitAsset.Colors.gray0.color + + [ + postSummaryCard, + + divider, + + applicantTitleLabel, + applicantTableView, + ].forEach { + contentView.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + NSLayoutConstraint.activate([ + postSummaryCard.topAnchor.constraint(equalTo: contentView.topAnchor), + postSummaryCard.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + postSummaryCard.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + + divider.topAnchor.constraint(equalTo: postSummaryCard.bottomAnchor, constant: 20), + divider.leftAnchor.constraint(equalTo: contentView.leftAnchor), + divider.rightAnchor.constraint(equalTo: contentView.rightAnchor), + + applicantTitleLabel.topAnchor.constraint(equalTo: divider.topAnchor, constant: 20), + applicantTitleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + + applicantTableView.topAnchor.constraint(equalTo: applicantTitleLabel.bottomAnchor, constant: 20), + applicantTableView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + applicantTableView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + applicantTableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + let scrollView = UIScrollView() + scrollView.delaysContentTouches = false + scrollView.contentInset = .init( + top: 36, + left: 0, + bottom: 20, + right: 0 + ) + let contentGuide = scrollView.contentLayoutGuide + let frameGuide = scrollView.frameLayoutGuide + + scrollView.addSubview(contentView) + contentView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + contentView.topAnchor.constraint(equalTo: contentGuide.topAnchor), + contentView.leftAnchor.constraint(equalTo: contentGuide.leftAnchor), + contentView.rightAnchor.constraint(equalTo: contentGuide.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor), + + contentView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor), + ]) + + [ + navigationBar, + scrollView, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 21), + navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 12), + + scrollView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor), + scrollView.leftAnchor.constraint(equalTo: view.leftAnchor), + scrollView.rightAnchor.constraint(equalTo: view.rightAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + } + + private func setTableView() { + applicantTableView.rowHeight = UITableView.automaticDimension + applicantTableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) + applicantTableView.isScrollEnabled = false + applicantTableView.dataSource = self + applicantTableView.delegate = self + applicantTableView.separatorStyle = .none + applicantTableView.delaysContentTouches = false + } + + private func setObservable() { } + + public func bind(viewModel: CheckApplicantViewModelable) { + + self.viewModel = viewModel + + navigationBar + .eventPublisher + .bind(to: viewModel.exitButtonClicked) + .disposed(by: disposeBag) + + postSummaryCard + .bind(vo: viewModel.postCardVO) + + viewModel + .postApplicantVO? + .drive(onNext: { [weak self] vo in + guard let self else { return } + postApplicantVO.accept(vo) + applicantTableView.reloadData() + }) + .disposed(by: disposeBag) + + rx.viewWillAppear + .map { _ in } + .bind(to: viewModel.requestpostApplicantVO) + .disposed(by: disposeBag) + } +} + +extension CheckApplicantVC: UITableViewDataSource, UITableViewDelegate { + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + postApplicantVO.value.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell + + if let viewModel = self.viewModel { + let vm = viewModel.createApplicantCardVM(vo: postApplicantVO.value[indexPath.row]) + cell.bind(viewModel: vm) + cell.selectionStyle = .none + } + + return cell + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift new file mode 100644 index 00000000..e514744a --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift @@ -0,0 +1,122 @@ +// +// CheckApplicantVM.swift +// CenterFeature +// +// Created by choijunios on 8/13/24. +// + +import Foundation +import RxSwift +import RxCocoa +import Entity +import DSKit +import PresentationCore + +public protocol CheckApplicantViewModelable { + // Input + var requestpostApplicantVO: PublishRelay { get } + var exitButtonClicked: PublishRelay { get } + + // Output + var postApplicantVO: Driver<[PostApplicantVO]>? { get } + var postCardVO: CenterEmployCardVO { get } + var alert: Driver? { get } + + func createApplicantCardVM(vo: PostApplicantVO) -> ApplicantCardVM +} + +public class CheckApplicantVM: CheckApplicantViewModelable { + + weak var coorindator: CheckApplicantCoordinatable? + + public var exitButtonClicked: PublishRelay = .init() + public var requestpostApplicantVO: PublishRelay = .init() + public var postCardVO: CenterEmployCardVO + + public var postApplicantVO: Driver<[PostApplicantVO]>? + public var alert: RxCocoa.Driver? + + let disposeBag = DisposeBag() + + public init(postCardVO: CenterEmployCardVO, coorindator: CheckApplicantCoordinatable?) { + self.postCardVO = postCardVO + self.coorindator = coorindator + + exitButtonClicked + .subscribe(onNext: { [weak self] _ in + + self?.coorindator?.taskFinished() + }) + .disposed(by: disposeBag) + + // Input + let requestPostApplicantVOResult = requestpostApplicantVO + .flatMap { [unowned self] _ in + publishPostApplicantVOMocks() + } + .share() + + let requestPostApplicantSuccess = requestPostApplicantVOResult.compactMap { $0.value } + let requestPostApplicantFailure = requestPostApplicantVOResult.compactMap { $0.error } + + // Output + postApplicantVO = requestPostApplicantSuccess.asDriver(onErrorJustReturn: []) + + alert = requestPostApplicantFailure + .map { error in + + DefaultAlertContentVO( + title: "시스템 오류", + message: error.message + ) + } + .asDriver(onErrorJustReturn: .default) + } + + public func createApplicantCardVM(vo: PostApplicantVO) -> ApplicantCardVM { + .init(vo: vo, coordinator: coorindator) + } + + func publishPostApplicantVOMocks() -> Single> { + + .just(.success((0...10).map { _ in PostApplicantVO.mock })) + } +} + + +// MARK: ApplicantCardVM +public class ApplicantCardVM: ApplicantCardViewModelable { + + // Init + let id: String + weak var coordinator: CheckApplicantCoordinatable? + + public var showProfileButtonClicked: PublishRelay = .init() + public var employButtonClicked: PublishRelay = .init() + public var staredThisWorker: PublishRelay = .init() + + public var renderObject: Driver? + + let disposeBag = DisposeBag() + + public init(vo: PostApplicantVO, coordinator: CheckApplicantCoordinatable?) { + self.id = vo.workerId + self.coordinator = coordinator + + // MARK: RenderObject + let publishRelay: BehaviorRelay = .init(value: .mock) + renderObject = publishRelay.asDriver(onErrorJustReturn: .mock) + + publishRelay.accept(ApplicantCardRO.create(vo: vo)) + + // MARK: 버튼 처리 + showProfileButtonClicked + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + coordinator?.showWorkerProfileScreen( + profileId: id + ) + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVC.swift new file mode 100644 index 00000000..6cd334b8 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVC.swift @@ -0,0 +1,182 @@ +// +// CenterRecruitmentPostBoardVC.swift +// CenterFeature +// +// Created by choijunios on 8/12/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class CenterRecruitmentPostBoardVC: BaseViewController { + enum TabBarState: Int, CaseIterable { + case onGoingPost = 0 + case closedPost = 1 + + var titleText: String { + switch self { + case .onGoingPost: + "진행 중인 공고" + case .closedPost: + "이전 공고" + } + } + } + struct TabBarItem: IdleTabItem { + var id: TabBarState + var tabLabelText: String + + init(id: TabBarState) { + self.id = id + self.tabLabelText = id.titleText + } + } + + var viewModel: CenterRecruitmentPostBoardViewModelable? + + private var currentState: TabBarState = .onGoingPost + private let viewControllerDict: [TabBarState: UIViewController] = [ + .onGoingPost : OnGoingPostVC(), + .closedPost : ClosedPostVC() + ] + + // Init + + // View + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + label.textString = "공고 관리" + return label + }() + + lazy var tabBar: IdleTabControlBar = .init( + items: TabBarState.allCases.map { TabBarItem(id: $0) }, + initialItem: .init(id: currentState) + )! + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + setAppearance() + setLayout() + setObservable() + + addViewControllerAndSetLayout(vc: viewControllerDict[currentState]!) + } + + private func setAppearance() { + view.backgroundColor = DSKitAsset.Colors.gray0.color + } + + private func setLayout() { + [ + titleLabel, + tabBar, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 21), + titleLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + + tabBar.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + tabBar.leftAnchor.constraint(equalTo: view.leftAnchor), + tabBar.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + } + + private func setObservable() { + tabBar + .statePublisher + .subscribe(onNext: { [weak self] item in + self?.showViewController(state: item.id) + }) + .disposed(by: disposeBag) + } + + private func showViewController(state: TabBarState) { + + if currentState == state { return } + + // 탭바터치 정지 + tabBar.isUserInteractionEnabled = false + + /// viewWillAppear이후에 호출 + let prevViewController = viewControllerDict[currentState] + let vc = viewControllerDict[state]! + + let prevIndex = currentState.rawValue + let currentIndex = state.rawValue + + addViewControllerAndSetLayout(vc: vc) + + vc.view.transform = .init(translationX: view.bounds.width * (prevIndex < currentIndex ? 1 : -1), y: 0) + + UIView.animate(withDuration: 0.2) { [weak self] in + + guard let self else { return } + + vc.view.transform = .identity + prevViewController?.view.transform = .init(translationX: (prevIndex < currentIndex ? -1 : 1) * view.bounds.width, y: 0) + + } completion: { [weak self] _ in + + prevViewController?.view.removeFromSuperview() + + prevViewController?.willMove(toParent: nil) + prevViewController?.removeFromParent() + + self?.currentState = state + self?.tabBar.isUserInteractionEnabled = true + } + } + + private func addViewControllerAndSetLayout(vc: UIViewController) { + addChild(vc) + view.addSubview(vc.view) + vc.didMove(toParent: self) + vc.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + vc.view.topAnchor.constraint(equalTo: tabBar.bottomAnchor), + vc.view.leftAnchor.constraint(equalTo: view.leftAnchor), + vc.view.rightAnchor.constraint(equalTo: view.rightAnchor), + vc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + public func bind(viewModel: CenterRecruitmentPostBoardViewModelable) { + + self.viewModel = viewModel + + (viewControllerDict[.onGoingPost] as? OnGoingPostVC)?.bind(viewModel: viewModel) + (viewControllerDict[.closedPost] as? ClosedPostVC)?.bind(viewModel: viewModel) + + viewModel + .alert? + .drive(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + CenterRecruitmentPostBoardVC() +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift new file mode 100644 index 00000000..1587474d --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/CenterRecruitmentPostBoardVM.swift @@ -0,0 +1,124 @@ +// +// CenterRecruitmentPostBoardVM.swift +// CenterFeature +// +// Created by choijunios on 8/13/24. +// + +import Foundation +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public protocol CenterRecruitmentPostBoardViewModelable: OnGoingPostViewModelable & ClosedPostViewModelable { + var alert: Driver? { get } +} + + +public class CenterRecruitmentPostBoardVM: CenterRecruitmentPostBoardViewModelable { + + weak var coordinator: RecruitmentManagementCoordinatable? + + public var requestOngoingPost: PublishRelay = .init() + public var requestClosedPost: PublishRelay = .init() + + public var ongoingPostCardVO: Driver<[CenterEmployCardVO]>? + public var closedPostCardVO: Driver<[CenterEmployCardVO]>? + + public var alert: Driver? + + public init(coordinator: RecruitmentManagementCoordinatable?) { + self.coordinator = coordinator + + let requestOngoingPostResult = requestOngoingPost + .flatMap { [unowned self] _ in + publishOngoingPostMocks() + } + .share() + + let requestOngoingPostSuccess = requestOngoingPostResult.compactMap { $0.value } + let requestOngoingPostFailure = requestOngoingPostResult.compactMap { $0.error } + + ongoingPostCardVO = requestOngoingPostSuccess.asDriver(onErrorJustReturn: []) + + + let requestClosedPostResult = requestClosedPost + .flatMap { [unowned self] _ in + publishClosedPostMocks() + } + .share() + + let requestClosedPostSuccess = requestClosedPostResult.compactMap { $0.value } + let requestClosedPostFailure = requestClosedPostResult.compactMap { $0.error } + + closedPostCardVO = requestClosedPostSuccess.asDriver(onErrorJustReturn: []) + + alert = Observable.merge( + requestOngoingPostFailure, + requestClosedPostFailure + ).map { error in + DefaultAlertContentVO( + 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 createCellVM(vo: CenterEmployCardVO) -> any CenterEmployCardViewModelable { + CenterEmployCardVM( + vo: vo, + coordinator: coordinator + ) + } +} + +// MARK: 카드 뷰에 사용될 ViewModel +class CenterEmployCardVM: CenterEmployCardViewModelable { + + weak var coordinator: RecruitmentManagementCoordinatable? + + // Init + let id: String + + // Output + var renderObject: Driver? + + // Input + var cardClicked: PublishRelay = .init() + var checkApplicantBtnClicked: PublishRelay = .init() + var editPostBtnClicked: PublishRelay = .init() + var terminatePostBtnClicked: PublishRelay = .init() + + let disposeBag = DisposeBag() + + init(vo: CenterEmployCardVO, coordinator: RecruitmentManagementCoordinatable? = nil) { + self.id = vo.postId + self.coordinator = coordinator + + // MARK: RenderObject + let publishRelay: BehaviorRelay = .init(value: .mock) + renderObject = publishRelay.asDriver(onErrorJustReturn: .mock) + + publishRelay.accept(CenterEmployCardRO.create(vo)) + + // MARK: 버튼 처리 + checkApplicantBtnClicked + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.showCheckingApplicantScreen(vo) + }) + .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 new file mode 100644 index 00000000..031381bb --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/ClosedPostVC.swift @@ -0,0 +1,137 @@ +// +// ClosedPostVC.swift +// CenterFeature +// +// Created by choijunios on 8/13/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public protocol ClosedPostViewModelable { + + var closedPostCardVO: Driver<[CenterEmployCardVO]>? { get } + var requestClosedPost: PublishRelay { get } + + func createCellVM(vo: CenterEmployCardVO) -> CenterEmployCardViewModelable +} + +public class ClosedPostVC: BaseViewController { + + typealias Cell = CenterEmployCardCell + + var viewModel: ClosedPostViewModelable? + + // View + let postTableView: UITableView = { + let tableView = UITableView() + tableView.rowHeight = UITableView.automaticDimension + tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) + return tableView + }() + + let tableHeader = BoardSortigHeaderView() + + // Observable + private let disposeBag = DisposeBag() + + let closedPostCardVO: BehaviorRelay<[CenterEmployCardVO]> = .init(value: []) + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + setAppearance() + setLayout() + setObservable() + setTableView() + } + + private func setTableView() { + postTableView.dataSource = self + postTableView.delegate = self + postTableView.separatorStyle = .none + postTableView.delaysContentTouches = false + + postTableView.tableHeaderView = tableHeader + + tableHeader.frame = .init(origin: .zero, size: .init( + width: view.bounds.width, + height: 60) + ) + } + + private func setAppearance() { + + } + + private func setLayout() { + [ + postTableView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + postTableView.topAnchor.constraint(equalTo: view.topAnchor), + postTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + postTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + postTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + private func setObservable() { + + } + + public func bind(viewModel: ClosedPostViewModelable) { + + self.viewModel = viewModel + + // Output + viewModel + .closedPostCardVO? + .drive(onNext: { [weak self] vos in + guard let self else { return } + closedPostCardVO.accept(vos) + postTableView.reloadData() + }) + .disposed(by: disposeBag) + + // Input + rx.viewWillAppear + .map { _ in } + .bind(to: viewModel.requestClosedPost) + .disposed(by: disposeBag) + } +} + +extension ClosedPostVC: UITableViewDataSource, UITableViewDelegate { + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + closedPostCardVO.value.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell + cell.selectionStyle = .none + + if let viewModel = self.viewModel { + let vo = closedPostCardVO.value[indexPath.row] + let vm = viewModel.createCellVM(vo: vo) + 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 new file mode 100644 index 00000000..1f8bcce9 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/Post/SubVC/OnGoingPostVC.swift @@ -0,0 +1,176 @@ +// +// OnGoingPostVC.swift +// CenterFeature +// +// Created by choijunios on 8/13/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public protocol OnGoingPostViewModelable { + + var ongoingPostCardVO: Driver<[CenterEmployCardVO]>? { get } + var requestOngoingPost: PublishRelay { get } + + func createCellVM(vo: CenterEmployCardVO) -> CenterEmployCardViewModelable +} + +public class OnGoingPostVC: BaseViewController { + + typealias Cell = CenterEmployCardCell + + var viewModel: OnGoingPostViewModelable? + + // View + let postTableView: UITableView = { + let tableView = UITableView() + tableView.rowHeight = UITableView.automaticDimension + tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier) + return tableView + }() + + let tableHeader = BoardSortigHeaderView() + + let ongoingPostCardVO: BehaviorRelay<[CenterEmployCardVO]> = .init(value: [.mock]) + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public override func viewDidLoad() { + super.viewDidLoad() + setAppearance() + setLayout() + setObservable() + setTableView() + } + + private func setTableView() { + postTableView.dataSource = self + postTableView.delegate = self + postTableView.separatorStyle = .none + postTableView.delaysContentTouches = false + + postTableView.tableHeaderView = tableHeader + + tableHeader.frame = .init(origin: .zero, size: .init( + width: view.bounds.width, + height: 60) + ) + } + + private func setAppearance() { + + } + + private func setLayout() { + + [ + postTableView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + postTableView.topAnchor.constraint(equalTo: view.topAnchor), + postTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + postTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + postTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + } + + private func setObservable() { + + } + + public func bind(viewModel: OnGoingPostViewModelable) { + + self.viewModel = viewModel + + // Output + viewModel + .ongoingPostCardVO? + .drive(onNext: { [weak self] vos in + guard let self else { return } + ongoingPostCardVO.accept(vos) + postTableView.reloadData() + }) + .disposed(by: disposeBag) + + // Input + rx.viewWillAppear + .map { _ in } + .bind(to: viewModel.requestOngoingPost) + .disposed(by: disposeBag) + } +} + +extension OnGoingPostVC: UITableViewDataSource, UITableViewDelegate { + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + ongoingPostCardVO.value.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell + cell.selectionStyle = .none + + if let viewModel = self.viewModel { + let vo = ongoingPostCardVO.value[indexPath.row] + let vm = viewModel.createCellVM(vo: vo) + cell.bind(viewModel: vm) + } + + return cell + } +} + +class BoardSortigHeaderView: UIView { + + let sortingTypeButton: ImageTextButton = { + let button = ImageTextButton( + iconImage: DSKitAsset.Icons.chevronDown.image, + position: .postfix + ) + button.label.textString = "정렬 기준" + button.label.attrTextColor = DSKitAsset.Colors.gray300.color + return button + }() + + init() { + super.init(frame: .zero) + setLayout() + } + + required init?(coder: NSCoder) { fatalError() } + + func setLayout() { + + [ + sortingTypeButton + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + sortingTypeButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 24), + sortingTypeButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -24), + sortingTypeButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -12), + ]) + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift index 68bb72b7..eb81b020 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Overview/PostOverviewVC.swift @@ -313,7 +313,7 @@ public class PostOverviewVC: BaseViewController { .disposed(by: disposeBag) - // 앞전가지 입력한 정보를 저장합니다. + // 앞전까지 입력한 정보를 저장합니다. viewModel.updateToState() // 화면이 등장할 때마다 유효한 상태를 불러옵니다. diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/ApplocationDetail/ApplicationDetailView.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/ApplocationDetail/ApplicationDetailView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/ApplocationDetail/ApplicationDetailView.swift rename to project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/ApplocationDetail/ApplicationDetailView.swift diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/CustomerInformation/CustomerInformationView.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/CustomerInformation/CustomerInformationView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/CustomerInformation/CustomerInformationView.swift rename to project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/CustomerInformation/CustomerInformationView.swift diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/CustomerRequirement/CustomerRequirementView.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/CustomerRequirement/CustomerRequirementView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/CustomerRequirement/CustomerRequirementView.swift rename to project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/CustomerRequirement/CustomerRequirementView.swift diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/EditPost/EditPostVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/EditPost/EditPostVC.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/EditPost/EditPostVC.swift rename to project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/EditPost/EditPostVC.swift diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/RegisterCompleteScreen/RegisterCompleteVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterCompleteScreen/RegisterCompleteVC.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/RegisterCompleteScreen/RegisterCompleteVC.swift rename to project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterCompleteScreen/RegisterCompleteVC.swift diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/RegisterRecruitmentPostVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterRecruitmentPostVC.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/RegisterRecruitmentPostVC.swift rename to project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/RegisterRecruitmentPostVC.swift diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/WorkTimeAndPay/WorkTimeAndPayView.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/WorkTimeAndPay/WorkTimeAndPayView.swift similarity index 100% rename from project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/WorkTimeAndPay/WorkTimeAndPayView.swift rename to project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Register/WorkTimeAndPay/WorkTimeAndPayView.swift diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/CheckApplicantCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/CheckApplicantCoordinator.swift new file mode 100644 index 00000000..e1cde0fa --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/CheckApplicantCoordinator.swift @@ -0,0 +1,81 @@ +// +// CheckApplicantCoordinator.swift +// RootFeature +// +// Created by choijunios on 8/13/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity +import CenterFeature +import WorkerFeature +import BaseFeature + +public class CheckApplicantCoordinator: CheckApplicantCoordinatable { + + public var childCoordinators: [any PresentationCore.Coordinator] = [] + + public struct Dependency { + let navigationController: UINavigationController + let centerEmployCardVO: CenterEmployCardVO + let workerProfileUseCase: WorkerProfileUseCase + + public init(navigationController: UINavigationController, centerEmployCardVO: CenterEmployCardVO, workerProfileUseCase: WorkerProfileUseCase) { + self.navigationController = navigationController + self.centerEmployCardVO = centerEmployCardVO + self.workerProfileUseCase = workerProfileUseCase + } + } + + public weak var viewControllerRef: UIViewController? + public weak var parent: ParentCoordinator? + + public let navigationController: UINavigationController + let centerEmployCardVO: CenterEmployCardVO + let workerProfileUseCase: WorkerProfileUseCase + + public init( + dependency: Dependency + ) { + self.navigationController = dependency.navigationController + self.centerEmployCardVO = dependency.centerEmployCardVO + self.workerProfileUseCase = dependency.workerProfileUseCase + } + + deinit { + printIfDebug("\(String(describing: RegisterRecruitmentCoordinator.self))") + } + + public func start() { + let vc = CheckApplicantVC() + let vm = CheckApplicantVM( + postCardVO: centerEmployCardVO, + coorindator: self + ) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } +} + +extension CheckApplicantCoordinator { + + public func taskFinished() { + popViewController() + parent?.removeChildCoordinator(self) + } + + public func showWorkerProfileScreen(profileId: String) { + let coordinator = WorkerProfileCoordinator( + dependency: .init( + profileMode: .otherProfile(id: profileId), + navigationController: navigationController, + workerProfileUseCase: workerProfileUseCase + ) + ) + addChildCoordinator(coordinator) + coordinator.start() + } +} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift index 0e0b7820..57671e8a 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentManagementCoordinator.swift @@ -6,9 +6,27 @@ // import UIKit +import CenterFeature import PresentationCore +import UseCaseInterface +import Entity -public class RecruitmentManagementCoordinator: ChildCoordinator { + +public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatable { + + public struct Dependency { + weak var parent: CenterMainCoordinatable? + let navigationController: UINavigationController + let workerProfileUseCase: WorkerProfileUseCase + + public init(parent: CenterMainCoordinatable? = nil, navigationController: UINavigationController, workerProfileUseCase: WorkerProfileUseCase) { + self.parent = parent + self.navigationController = navigationController + self.workerProfileUseCase = workerProfileUseCase + } + } + + public var childCoordinators: [any PresentationCore.Coordinator] = [] public weak var viewControllerRef: UIViewController? @@ -16,31 +34,42 @@ public class RecruitmentManagementCoordinator: ChildCoordinator { public weak var parent: CenterMainCoordinatable? + let workerProfileUseCase: WorkerProfileUseCase + public init( - parent: CenterMainCoordinatable, - navigationController: UINavigationController + dependency: Dependency ) { - self.parent = parent - self.navigationController = navigationController + self.parent = dependency.parent + self.navigationController = dependency.navigationController + self.workerProfileUseCase = dependency.workerProfileUseCase } public func start() { - let vc = RecuitmentManagementVC(coordinator: self) + let vc = CenterRecruitmentPostBoardVC() + let vm = CenterRecruitmentPostBoardVM(coordinator: self) + vc.bind(viewModel: vm) + viewControllerRef = vc navigationController.pushViewController(vc, animated: false) } public func coordinatorDidFinish() { - + popViewController() + parent?.removeChildCoordinator(self) } } -extension RecruitmentManagementCoordinator { - - func showCenterRegisterScreen() { - parent?.centerProfileRegister() - } - - func showRegisterRecruitmentPostScreen() { - parent?.registerRecruitmentPost() +public extension RecruitmentManagementCoordinator { + + func showCheckingApplicantScreen(_ centerEmployCardVO: CenterEmployCardVO) { + let coordinator = CheckApplicantCoordinator( + dependency: .init( + navigationController: navigationController, + centerEmployCardVO: centerEmployCardVO, + workerProfileUseCase: workerProfileUseCase + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() } } diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift index b296948a..4532a107 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift @@ -41,17 +41,6 @@ public class RecuitmentManagementVC: UIViewController { button2.setTitleColor(.black, for: .normal) button2.isUserInteractionEnabled = true - button1.rx.tap - .subscribe { [weak coordinator] _ in - coordinator?.showCenterRegisterScreen() - } - .disposed(by: dispoesBag) - - button2.rx.tap - .subscribe { [weak coordinator] _ in - coordinator?.showRegisterRecruitmentPostScreen() - } - .disposed(by: dispoesBag) [ label, button1, diff --git a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift index 642cc560..f4d784fc 100644 --- a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift @@ -32,7 +32,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { repository: DefaultUserProfileRepository(store) ) - let vm = WorkerMyProfileViewModel(workerProfileUseCase: useCase) + let vm = WorkerMyProfileViewModel( + coordinator: nil, + workerProfileUseCase: useCase + ) let vc = WorkerProfileViewController() diff --git a/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Profile/WorkerProfileCoordinator.swift b/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Profile/WorkerProfileCoordinator.swift new file mode 100644 index 00000000..0c775251 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Profile/WorkerProfileCoordinator.swift @@ -0,0 +1,73 @@ +// +// WorkerProfileCoordinator.swift +// WorkerFeature +// +// Created by choijunios on 8/14/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + + +public class WorkerProfileCoordinator: ChildCoordinator { + + public struct Dependency { + public let profileMode: ProfileMode + public let navigationController: UINavigationController + public let workerProfileUseCase: WorkerProfileUseCase + + public init(profileMode: ProfileMode, navigationController: UINavigationController, workerProfileUseCase: WorkerProfileUseCase) { + self.profileMode = profileMode + self.navigationController = navigationController + self.workerProfileUseCase = workerProfileUseCase + } + } + + public weak var viewControllerRef: UIViewController? + public weak var parent: ParentCoordinator? + + public let navigationController: UINavigationController + let profileMode: ProfileMode + let workerProfileUseCase: WorkerProfileUseCase + + public init( + dependency: Dependency + ) { + self.navigationController = dependency.navigationController + self.profileMode = dependency.profileMode + self.workerProfileUseCase = dependency.workerProfileUseCase + } + + deinit { + printIfDebug("\(String(describing: WorkerProfileCoordinator.self))") + } + + public func start() { + let vc = WorkerProfileViewController() + + switch profileMode { + case .myProfile: + let vm = WorkerMyProfileViewModel( + coordinator: self, + workerProfileUseCase: workerProfileUseCase + ) + vc.bind(vm) + case .otherProfile(let id): + let vm = WorkerProfileViewModel( + coordinator: self, + workerProfileUseCase: workerProfileUseCase, + workerId: id + ) + vc.bind(vm) + } + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + popViewController(animated: true) + parent?.removeChildCoordinator(self) + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/EditWorkerProfileViewController.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/EditWorkerProfileViewController.swift similarity index 93% rename from project/Projects/Presentation/Feature/Worker/Sources/View/profile/EditWorkerProfileViewController.swift rename to project/Projects/Presentation/Feature/Worker/Sources/View/Profile/EditWorkerProfileViewController.swift index 3467c0c9..c7dcc7a0 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/EditWorkerProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/EditWorkerProfileViewController.swift @@ -12,21 +12,6 @@ import RxCocoa import DSKit import Entity import BaseFeature -import Kingfisher - -protocol WorkerProfileEditViewModelable: WorkerProfileViewModelable { - - var requestUpload: PublishRelay { get } - var editingImage: PublishRelay { get } - var editingIsJobFinding: PublishRelay { get } - var editingExpYear: PublishRelay { get } - var editingAddress: PublishRelay { get } - var editingIntroduce: PublishRelay { get } - var editingSpecialty: PublishRelay { get } - - var uploadSuccess: Driver? { get } - var alert: Driver? { get } -} public class EditWorkerProfileViewController: BaseViewController { @@ -45,23 +30,14 @@ public class EditWorkerProfileViewController: BaseViewController { }() // 프로필 이미지 - let profileImageContainer: UIView = { + let profileImageContainer: UIImageView = { - let view = UIView() + let view = UIImageView() view.backgroundColor = DSKitAsset.Colors.orange100.color view.layer.cornerRadius = 48 view.clipsToBounds = true - - /// PlaceHolderImage - let imageView = DSKitAsset.Icons.workerProfilePlaceholder.image.toView() - view.addSubview(imageView) - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 26), - imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), - imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -29), - imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -27), - ]) + view.image = DSKitAsset.Icons.workerProfilePlaceholder.image + view.contentMode = .scaleAspectFit return view }() diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/WorkerProfileViewController.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/WorkerProfileViewController.swift similarity index 65% rename from project/Projects/Presentation/Feature/Worker/Sources/View/profile/WorkerProfileViewController.swift rename to project/Projects/Presentation/Feature/Worker/Sources/View/Profile/WorkerProfileViewController.swift index 81489cbb..2572f87e 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/profile/WorkerProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/Profile/WorkerProfileViewController.swift @@ -12,36 +12,14 @@ import RxCocoa import DSKit import Entity import BaseFeature -import Kingfisher - -public protocol WorkerProfileViewModelable { - - // Input - var viewWillAppear: PublishRelay { get } - - // Output - var profileRenderObject: Driver? { get } -} - -extension UIImageView { - - func setImage(url: URL) { - let pngSerializer = FormatIndicatedCacheSerializer.png - self - .kf.setImage( - with: url, - options: [.cacheSerializer(pngSerializer)] - ) - } -} public class WorkerProfileViewController: DisposableViewController { private var viewModel: (any WorkerProfileViewModelable)? // 네비게이션 바 - let navigationBar: NavigationBarType1 = { - let bar = NavigationBarType1(navigationTitle: "") + lazy var navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar(titleText: "", innerViews: [profileEditButton]) return bar }() @@ -58,22 +36,14 @@ public class WorkerProfileViewController: DisposableViewController { }() // 프로필 이미지 - let profileImageContainer: UIView = { + let profileImageContainer: UIImageView = { - let view = UIView() + let view = UIImageView() view.backgroundColor = DSKitAsset.Colors.orange100.color view.layer.cornerRadius = 48 view.clipsToBounds = true - - let imageView = DSKitAsset.Icons.workerProfilePlaceholder.image.toView() - view.addSubview(imageView) - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 26), - imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), - imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -29), - imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -27), - ]) + view.image = DSKitAsset.Icons.workerProfilePlaceholder.image + view.contentMode = .scaleAspectFit return view }() @@ -87,6 +57,16 @@ public class WorkerProfileViewController: DisposableViewController { return imageView }() + // 즐겨찾기 버튼 +// public let starButton: IconWithColorStateButton = { +// let button = IconWithColorStateButton( +// representImage: DSKitAsset.Icons.subscribeStar.image, +// normalColor: DSKitAsset.Colors.gray200.color, +// accentColor: DSKitAsset.Colors.orange300.color +// ) +// return button +// }() + // 구인중 / 휴식중 let workingTag: TagLabel = { let label = TagLabel( @@ -146,6 +126,21 @@ public class WorkerProfileViewController: DisposableViewController { return label }() + // 통화하기 버튼 + // 통화하기, 채팅하기 + lazy var contactButtonContainer: HStack = .init( + [phoneCallButton], + spacing: 8, + distribution: .fillEqually + ) + let phoneCallButton: IdleSecondaryButton = { + let button = IdleSecondaryButton(level: .medium) + button.label.textString = "통화하기" + return button + }() + + // MARK: 상세정보 + // 주소(Label + Content) let addressTitleLabel: IdleLabel = { let label = IdleLabel(typography: .Subtitle4) @@ -200,25 +195,11 @@ public class WorkerProfileViewController: DisposableViewController { } private func setApearance() { - view.backgroundColor = .white - view.layoutMargins = .init( - top: 0, - left: 20, - bottom: 0, - right: 20 - ) + view.backgroundColor = DSKitAsset.Colors.gray050.color } private func setAutoLayout() { - // 상단 네비게이션바 세팅 - let navigationStack = HStack([ - navigationBar, - profileEditButton, - ]) - navigationStack.distribution = .equalSpacing - navigationStack.backgroundColor = .clear - // 흑색 바탕 let grayBackgrounnd = UIView() grayBackgrounnd.backgroundColor = DSKitAsset.Colors.gray050.color @@ -284,6 +265,7 @@ public class WorkerProfileViewController: DisposableViewController { } } + // MARK: Divider // 요양보호사 인적정보 / 요양보호사 구직정보 디바이더 let divider = UIView() divider.backgroundColor = DSKitAsset.Colors.gray050.color @@ -313,64 +295,141 @@ public class WorkerProfileViewController: DisposableViewController { spacing: 28, alignment: .leading) - // view hierarchy + let contentView = UIView() + contentView.backgroundColor = DSKitAsset.Colors.gray0.color + contentView.layoutMargins = .init( + top: 0, + left: 20, + bottom: 54, + right: 20 + ) + [ grayBackgrounnd, - navigationStack, profileImageContainer, +// starButton, tagNameStack, humanInfoStack, + contactButtonContainer, divider, employeeInfoTitleLabel, employeeInfoStack ].forEach { $0.translatesAutoresizingMaskIntoConstraints = false - view.addSubview($0) + contentView.addSubview($0) } - grayBackgrounnd.layer.zPosition = 0.0 - navigationStack.layer.zPosition = 1.0 NSLayoutConstraint.activate([ - grayBackgrounnd.topAnchor.constraint(equalTo: view.topAnchor), - grayBackgrounnd.leadingAnchor.constraint(equalTo: view.leadingAnchor), - grayBackgrounnd.trailingAnchor.constraint(equalTo: view.trailingAnchor), - grayBackgrounnd.heightAnchor.constraint(equalToConstant: 196), - - navigationStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 21), - navigationStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12), - navigationStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + grayBackgrounnd.topAnchor.constraint(equalTo: contentView.topAnchor), + grayBackgrounnd.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + grayBackgrounnd.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + grayBackgrounnd.heightAnchor.constraint(equalToConstant: 80), profileImageContainer.widthAnchor.constraint(equalToConstant: 96), profileImageContainer.heightAnchor.constraint(equalTo: profileImageContainer.widthAnchor), - profileImageContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), + profileImageContainer.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), profileImageContainer.centerYAnchor.constraint(equalTo: grayBackgrounnd.bottomAnchor), +// starButton.widthAnchor.constraint(equalToConstant: 24), +// starButton.heightAnchor.constraint(equalTo: starButton.widthAnchor), +// starButton.topAnchor.constraint(equalTo: grayBackgrounnd.bottomAnchor, constant: 20), +// starButton.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + tagNameStack.topAnchor.constraint(equalTo: profileImageContainer.bottomAnchor, constant: 16), - tagNameStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + tagNameStack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), humanInfoStack.topAnchor.constraint(equalTo: tagNameStack.bottomAnchor, constant: 16), - humanInfoStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + humanInfoStack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - divider.topAnchor.constraint(equalTo: humanInfoStack.bottomAnchor, constant: 24), - divider.leadingAnchor.constraint(equalTo: view.leadingAnchor), - divider.trailingAnchor.constraint(equalTo: view.trailingAnchor), + contactButtonContainer.topAnchor.constraint(equalTo: humanInfoStack.bottomAnchor, constant: 24), + contactButtonContainer.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), + contactButtonContainer.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + + divider.topAnchor.constraint(equalTo: contactButtonContainer.bottomAnchor, constant: 24), + divider.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + divider.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), divider.heightAnchor.constraint(equalToConstant: 8), employeeInfoTitleLabel.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 24), - employeeInfoTitleLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + employeeInfoTitleLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), employeeInfoStack.topAnchor.constraint(equalTo: employeeInfoTitleLabel.bottomAnchor, constant: 20), - employeeInfoStack.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), - employeeInfoStack.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + employeeInfoStack.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + employeeInfoStack.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), + employeeInfoStack.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), + ]) + + let scrollView = UIScrollView() + scrollView.delaysContentTouches = false + let contentGuide = scrollView.contentLayoutGuide + let frameGuide = scrollView.frameLayoutGuide + + scrollView.addSubview(contentView) + contentView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + contentView.topAnchor.constraint(equalTo: contentGuide.topAnchor), + contentView.leftAnchor.constraint(equalTo: contentGuide.leftAnchor), + contentView.rightAnchor.constraint(equalTo: contentGuide.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor), + + contentView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor), + ]) + + + [ + navigationBar, + scrollView, + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + scrollView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor), + scrollView.leftAnchor.constraint(equalTo: view.leftAnchor), + scrollView.rightAnchor.constraint(equalTo: view.rightAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } - public func bind(_ viewModel: any WorkerProfileViewModelable) { + private func setObservable() { + + navigationBar + .backButton.rx.tap + .observe(on: MainScheduler.instance) + .subscribe { [weak self] _ in + self?.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + } + + /// 다른 프로필 확인용 바인딩 입니다. + public func bind(_ viewModel: OtherWorkerProfileViewModelable) { + bind(viewModel as WorkerProfileViewModelable) + + phoneCallButton + .rx.tap + .bind(to: viewModel.phoneCallButtonClicked) + .disposed(by: disposeBag) + } + + /// 내프로필 접속용 바인딩 입니다. + public func bind(_ viewModel: WorkerProfileEditViewModelable) { + bind(viewModel as WorkerProfileViewModelable) + } + + private func bind(_ viewModel: any WorkerProfileViewModelable) { self.viewModel = viewModel + // Input profileEditButton .eventPublisher .observe(on: MainScheduler.instance) @@ -382,13 +441,18 @@ public class WorkerProfileViewController: DisposableViewController { }) .disposed(by: disposeBag) - // Input + navigationBar + .backButton.rx.tap + .bind(to: viewModel.exitButtonClicked) + .disposed(by: disposeBag) + self.rx .viewWillAppear .filter { $0 } .map { _ in () } .bind(to: viewModel.viewWillAppear) .disposed(by: disposeBag) + // Output viewModel @@ -398,16 +462,19 @@ public class WorkerProfileViewController: DisposableViewController { guard let self else { return } // UI 업데이트 - navigationBar.navigationTitle = ro.navigationTitle + navigationBar.titleLabel.textString = ro.navigationTitle profileEditButton.isHidden = !ro.showEditButton + contactButtonContainer.isHidden = !ro.showContactButton +// starButton.isHidden = !ro.showStarButton workingTag.textString = ro.stateText nameLabel.textString = ro.nameText ageLabel.textString = ro.ageText genderLabel.textString = ro.genderText expLabel.textString = ro.expText + addressLabel.textString = ro.address - introductionLabel.textString = ro.oneLineIntroduce - abilityLabel.textString = ro.specialty + introductionLabel.textString = ro.oneLineIntroduce.emptyDefault("-") + abilityLabel.textString = ro.specialty.emptyDefault("-") if let imageUrl = ro.imageUrl { workerProfileImage.setImage(url: imageUrl) @@ -421,6 +488,8 @@ public class WorkerProfileViewController: DisposableViewController { } } + + @available(iOS 17.0, *) #Preview("Preview", traits: .defaultLayout) { diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/RenderObject/WorkerProfileRenderObject.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/RenderObject/WorkerProfileRenderObject.swift index 66b0531c..ab991549 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/RenderObject/WorkerProfileRenderObject.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RenderObject/WorkerProfileRenderObject.swift @@ -13,6 +13,8 @@ public struct WorkerProfileRenderObject { let navigationTitle: String let showEditButton: Bool + let showContactButton: Bool +// let showStarButton: Bool let isJobFinding: Bool let stateText: String let nameText: String @@ -30,6 +32,8 @@ public struct WorkerProfileRenderObject { .init( navigationTitle: isMyProfile ? "내 프로필" : "요양보호사 프로필", showEditButton: isMyProfile, + showContactButton: !isMyProfile, +// showStarButton: !isMyProfile, isJobFinding: vo.isLookingForJob, stateText: vo.isLookingForJob ? "구인중" : "휴식중", nameText: vo.nameText, diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/profile/WorkerMyProfileViewModel.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerMyProfileViewModel.swift similarity index 80% rename from project/Projects/Presentation/Feature/Worker/Sources/ViewModel/profile/WorkerMyProfileViewModel.swift rename to project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerMyProfileViewModel.swift index 474bf458..5cb5a7d3 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/profile/WorkerMyProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerMyProfileViewModel.swift @@ -13,24 +13,42 @@ import DSKit import Entity import UseCaseInterface +public protocol WorkerProfileEditViewModelable: WorkerProfileViewModelable { + + var requestUpload: PublishRelay { get } + var editingImage: PublishRelay { get } + var editingIsJobFinding: PublishRelay { get } + var editingExpYear: PublishRelay { get } + var editingAddress: PublishRelay { get } + var editingIntroduce: PublishRelay { get } + var editingSpecialty: PublishRelay { get } + + var uploadSuccess: Driver? { get } + var alert: Driver? { get } +} + public class WorkerMyProfileViewModel: WorkerProfileEditViewModelable { + public weak var coordinator: WorkerProfileCoordinator? + let workerProfileUseCase: WorkerProfileUseCase // Input(Editing) - var requestUpload: PublishRelay = .init() - var editingImage: PublishRelay = .init() - var editingIsJobFinding: PublishRelay = .init() - var editingExpYear: PublishRelay = .init() - var editingAddress: PublishRelay = .init() - var editingIntroduce: PublishRelay = .init() - var editingSpecialty: PublishRelay = .init() + public var requestUpload: PublishRelay = .init() + public var editingImage: PublishRelay = .init() + public var editingIsJobFinding: PublishRelay = .init() + public var editingExpYear: PublishRelay = .init() + public var editingAddress: PublishRelay = .init() + public var editingIntroduce: PublishRelay = .init() + public var editingSpecialty: PublishRelay = .init() // Input(Rendering) public var viewWillAppear: PublishRelay = .init() + public var exitButtonClicked: RxRelay.PublishRelay = .init() + // Output - var uploadSuccess: Driver? + public var uploadSuccess: Driver? public var alert: Driver? public var profileRenderObject: Driver? @@ -43,8 +61,12 @@ public class WorkerMyProfileViewModel: WorkerProfileEditViewModelable { let disposbag: DisposeBag = .init() - public init(workerProfileUseCase: WorkerProfileUseCase) { + public init( + coordinator: WorkerProfileCoordinator?, + workerProfileUseCase: WorkerProfileUseCase + ) { + self.coordinator = coordinator self.workerProfileUseCase = workerProfileUseCase // Input(Rendering) @@ -80,6 +102,12 @@ public class WorkerMyProfileViewModel: WorkerProfileEditViewModelable { .bind(to: rederingState) .disposed(by: disposbag) + exitButtonClicked + .subscribe(onNext: { [weak self] in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposbag) + // Edit Input let imageValidationResult = editingImage diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/profile/WorkerProfileViewModel.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModel.swift similarity index 69% rename from project/Projects/Presentation/Feature/Worker/Sources/ViewModel/profile/WorkerProfileViewModel.swift rename to project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModel.swift index e523327c..0a07f9be 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/profile/WorkerProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModel.swift @@ -13,7 +13,15 @@ import DSKit import Entity import UseCaseInterface -public class WorkerProfileViewModel: WorkerProfileViewModelable { +/// 자신의 프로필을 확인하는 경우가 아닌 센터측에서 요양보호사를 보는 경우 +public protocol OtherWorkerProfileViewModelable: WorkerProfileViewModelable { + + var phoneCallButtonClicked: PublishRelay { get } +} + +public class WorkerProfileViewModel: OtherWorkerProfileViewModelable { + + public weak var coordinator: WorkerProfileCoordinator? let workerProfileUseCase: WorkerProfileUseCase @@ -22,6 +30,8 @@ public class WorkerProfileViewModel: WorkerProfileViewModelable { // Input(Rendering) public var viewWillAppear: PublishRelay = .init() + public var exitButtonClicked: PublishRelay = .init() + public var phoneCallButtonClicked: PublishRelay = .init() // Output var uploadSuccess: Driver? @@ -37,8 +47,13 @@ public class WorkerProfileViewModel: WorkerProfileViewModelable { let disposbag: DisposeBag = .init() - public init(workerProfileUseCase: WorkerProfileUseCase, workerId: String) { + public init( + coordinator: WorkerProfileCoordinator?, + workerProfileUseCase: WorkerProfileUseCase, + workerId: String + ) { + self.coordinator = coordinator self.workerProfileUseCase = workerProfileUseCase self.workerId = workerId @@ -66,6 +81,20 @@ public class WorkerProfileViewModel: WorkerProfileViewModelable { return vo } + exitButtonClicked + .subscribe(onNext: { [weak self] in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposbag) + + phoneCallButtonClicked + .subscribe(onNext: { _ in + + // 안심번호 전화연결 + + }) + .disposed(by: disposbag) + fetchedProfileVOSuccess .asObservable() .map({ vo in diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModelable.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModelable.swift new file mode 100644 index 00000000..c2b7a687 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModelable.swift @@ -0,0 +1,24 @@ +// +// File.swift +// WorkerFeature +// +// Created by choijunios on 8/14/24. +// + +import UIKit +import RxSwift +import RxCocoa +import Entity + + +public protocol WorkerProfileViewModelable { + + var coordinator: WorkerProfileCoordinator? { get } + + // Input + var viewWillAppear: PublishRelay { get } + var exitButtonClicked: PublishRelay { get } + + // Output + var profileRenderObject: Driver? { get } +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/Extensions/String+Extension.swift b/project/Projects/Presentation/PresentationCore/Sources/Extensions/String+Extension.swift new file mode 100644 index 00000000..51dab598 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/Extensions/String+Extension.swift @@ -0,0 +1,15 @@ +// +// String+Extension.swift +// PresentationCore +// +// Created by choijunios on 8/14/24. +// + +import Foundation + +public extension String { + + func emptyDefault(_ str: String) -> String { + self.isEmpty ? str : self + } +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Main/CenterMainCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Main/CenterMainCoordinatable.swift index 6760b66b..7fdb10a3 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Main/CenterMainCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Main/CenterMainCoordinatable.swift @@ -8,6 +8,5 @@ import Foundation public protocol CenterMainCoordinatable: ParentCoordinator { - func centerProfileRegister() - func registerRecruitmentPost() + } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CenterPostBoardCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CenterPostBoardCoordinatable.swift new file mode 100644 index 00000000..ea8a3374 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CenterPostBoardCoordinatable.swift @@ -0,0 +1,15 @@ +// +// CenterPostBoardCoordinatable.swift +// PresentationCore +// +// Created by choijunios on 8/13/24. +// + +import Foundation + +public protocol CenterPostBoardTabCoordinatable: ParentCoordinator { + + func showApplicantScreen() + func postDetailScreen() + func showPostEditScreen() +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CheckApplicantCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CheckApplicantCoordinatable.swift new file mode 100644 index 00000000..d4193255 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/CheckApplicantCoordinatable.swift @@ -0,0 +1,14 @@ +// +// CheckApplicantCoordinatable.swift +// PresentationCore +// +// Created by choijunios on 8/13/24. +// + +import Foundation + +public protocol CheckApplicantCoordinatable: ParentCoordinator { + + func taskFinished() + func showWorkerProfileScreen(profileId: String) +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift new file mode 100644 index 00000000..5ca23ec9 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/RecruitmentPost/RecruitmentManagementCoordinatable.swift @@ -0,0 +1,13 @@ +// +// RecruitmentManagementCoordinatable.swift +// PresentationCore +// +// Created by choijunios on 8/13/24. +// + +import Entity + +public protocol RecruitmentManagementCoordinatable: ParentCoordinator { + + func showCheckingApplicantScreen(_ centerEmployCardVO: CenterEmployCardVO) +}