diff --git a/Projects/Data/Sources/DTO/Applications/TotalPassStudentResponseDTO.swift b/Projects/Data/Sources/DTO/Applications/TotalPassStudentResponseDTO.swift index 5d6dfa26..c9f24c55 100644 --- a/Projects/Data/Sources/DTO/Applications/TotalPassStudentResponseDTO.swift +++ b/Projects/Data/Sources/DTO/Applications/TotalPassStudentResponseDTO.swift @@ -19,7 +19,8 @@ public struct TotalPassStudentResponseDTO: Codable { public extension TotalPassStudentResponseDTO { func toDomain() -> TotalPassStudentEntity { - TotalPassStudentEntity( + let totalStudentCount = totalStudentCount > 0 ? totalStudentCount: 1 + return TotalPassStudentEntity( totalStudentCount: totalStudentCount, passedCount: passedCount, approvedCount: approvedCount diff --git a/Projects/Domain/Sources/UseCases/Recruitments/FetchRecruitmentListUseCase.swift b/Projects/Domain/Sources/UseCases/Recruitments/FetchRecruitmentListUseCase.swift index a86e9127..f9358d6f 100644 --- a/Projects/Domain/Sources/UseCases/Recruitments/FetchRecruitmentListUseCase.swift +++ b/Projects/Domain/Sources/UseCases/Recruitments/FetchRecruitmentListUseCase.swift @@ -8,7 +8,7 @@ public struct FetchRecruitmentListUseCase { private let recruitmentsRepository: RecruitmentsRepository public func execute( - page: Int, jobCode: String?, techCode: [String]?, name: String? + page: Int, jobCode: String? = nil, techCode: [String]? = nil, name: String? = nil ) -> Single<[RecruitmentEntity]> { recruitmentsRepository.fetchRecruitmentList(page: page, jobCode: jobCode, techCode: techCode, name: name) } diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOff.imageset/Bookmark-1.svg b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark Off.imageset/Bookmark-1.svg similarity index 100% rename from Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOff.imageset/Bookmark-1.svg rename to Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark Off.imageset/Bookmark-1.svg diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOff.imageset/Contents.json b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark Off.imageset/Contents.json similarity index 100% rename from Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOff.imageset/Contents.json rename to Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark Off.imageset/Contents.json diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOn.imageset/Bookmark.svg b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark On.imageset/Bookmark.svg similarity index 100% rename from Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOn.imageset/Bookmark.svg rename to Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark On.imageset/Bookmark.svg diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOn.imageset/Contents.json b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark On.imageset/Contents.json similarity index 100% rename from Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/bookmarkOn.imageset/Contents.json rename to Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Bookmark On.imageset/Contents.json diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/searchIcon.imageset/Contents.json b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Search Icon.imageset/Contents.json similarity index 100% rename from Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/searchIcon.imageset/Contents.json rename to Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Search Icon.imageset/Contents.json diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/searchIcon.imageset/search.svg b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Search Icon.imageset/search.svg similarity index 100% rename from Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/searchIcon.imageset/search.svg rename to Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Search Icon.imageset/search.svg diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Sound.imageset/Contents.json b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Sound.imageset/Contents.json new file mode 100644 index 00000000..6f2dd8dd --- /dev/null +++ b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Sound.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "fluent_speaker-2-48-regular.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Sound.imageset/fluent_speaker-2-48-regular.svg b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Sound.imageset/fluent_speaker-2-48-regular.svg new file mode 100644 index 00000000..e407be70 --- /dev/null +++ b/Projects/Modules/DesignSystem/Resources/Images/Icons.xcassets/Sound.imageset/fluent_speaker-2-48-regular.svg @@ -0,0 +1,3 @@ + + + diff --git a/Projects/Modules/DesignSystem/Sources/Image/JobisIcon.swift b/Projects/Modules/DesignSystem/Sources/Image/JobisIcon.swift index 06aee370..8972a827 100644 --- a/Projects/Modules/DesignSystem/Sources/Image/JobisIcon.swift +++ b/Projects/Modules/DesignSystem/Sources/Image/JobisIcon.swift @@ -29,6 +29,7 @@ public enum JobisIcon { case currentPageControl case defaultPageControl case pieChart + case sound case trash case emptyBookmark @@ -100,6 +101,9 @@ public enum JobisIcon { case .pieChart: return dsIcons.pieChart.image + case .sound: + return dsIcons.sound.image + case .trash: return dsIcons.trash.image diff --git a/Projects/Presentation/Sources/Base/BaseBottomSheetViewController.swift b/Projects/Presentation/Sources/Base/BaseBottomSheetViewController.swift new file mode 100644 index 00000000..78b5dee9 --- /dev/null +++ b/Projects/Presentation/Sources/Base/BaseBottomSheetViewController.swift @@ -0,0 +1,251 @@ +import UIKit +import Then +import SnapKit +import RxGesture +import RxSwift +import RxCocoa +import DesignSystem + +public enum BottomSheetViewState { + case normal + case custom(height: CGFloat) +} + +public class BaseBottomSheetViewController: UIViewController, + ViewControllable, + LifeCyclePublishable, + HasDisposeBag, + AddViewable, + SetLayoutable, + Bindable, + ViewControllerConfigurable, + NavigationConfigurable { + public let viewModel: ViewModel + public var disposeBag = DisposeBag() + public var viewDidLoadPublisher = PublishRelay() + public var viewWillAppearPublisher = PublishRelay() + public var viewDidAppearPublisher = PublishRelay() + public var viewWillDisappearPublisher = PublishRelay() + public var viewDidDisappearPublisher = PublishRelay() + private lazy var dimmedView = UIView().then { + $0.backgroundColor = .black.withAlphaComponent(0.5) + $0.alpha = 0 + $0.isUserInteractionEnabled = true + } + private let bottomSheetView = UIView().then { + $0.backgroundColor = .clear + } + private let dragIndicatorView = UIView().then { + $0.backgroundColor = .white + $0.layer.cornerRadius = 2 + } + public let contentView = UIView().then { + $0.backgroundColor = .GrayScale.gray30 + $0.layer.cornerRadius = 16 + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + $0.clipsToBounds = true + } + private let state: BottomSheetViewState + private let bottomSheetPanMinTopInset: CGFloat = 50 + private let dragHeight = 28.0 + private var defaultHeight: CGFloat = 500 + private lazy var maxTopInset = ( + view.safeAreaInsets.bottom + view.safeAreaLayoutGuide.layoutFrame.height + ) + private lazy var bottomSheetViewTopInset: CGFloat = maxTopInset + private lazy var bottomSheetPanStartingTopInset: CGFloat = bottomSheetPanMinTopInset + + public init(_ viewModel: ViewModel, state: BottomSheetViewState = .normal) { + self.viewModel = viewModel + self.state = state + super .init(nibName: nil, bundle: nil) + self.modalPresentationStyle = .overFullScreen + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + self.addView() + self.setLayout() + } + + public override func viewDidLoad() { + super.viewDidLoad() + self.bind() + self.configureNavigation() + self.configureViewController() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.viewWillAppearPublisher.accept(()) + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.viewDidAppearPublisher.accept(()) + self.showBottomSheet(atState: state) + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.viewWillDisappearPublisher.accept(()) + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.viewDidDisappearPublisher.accept(()) + } + + public override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + self.view.endEditing(true) + } + + public func addView() { + [ + dimmedView, + bottomSheetView + ].forEach(view.addSubview(_:)) + [ + dragIndicatorView, + contentView + ].forEach(bottomSheetView.addSubview(_:)) + } + + public func setLayout() { + dimmedView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + bottomSheetView.snp.updateConstraints { + $0.leading.trailing.bottom.equalToSuperview() + $0.top.equalTo(view.safeAreaLayoutGuide).inset(bottomSheetViewTopInset) + } + dragIndicatorView.snp.makeConstraints { + $0.width.lessThanOrEqualTo(64) + $0.height.lessThanOrEqualTo(4) + $0.centerX.equalToSuperview() + $0.top.lessThanOrEqualToSuperview().inset(12) + } + contentView.snp.makeConstraints { + $0.top.equalTo(dragIndicatorView.snp.bottom).offset(12) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + public func bind() {} + + public func configureViewController() { + dimmedView.rx.tapGesture() + .when(.recognized) + .bind { [weak self] _ in + self?.dismissBottomSheet() + } + .disposed(by: disposeBag) + + bottomSheetView.rx.panGesture() + .skip(1) + .bind(with: self, onNext: { owner, gesture in + let translation = gesture.translation(in: owner.view) + let defaultPadding = owner.maxTopInset - owner.defaultHeight + + switch gesture.state { + case .began: + owner.bottomSheetPanStartingTopInset = owner.bottomSheetViewTopInset + + case .changed: + if translation.y > 0 { + owner.bottomSheetViewTopInset = owner + .bottomSheetPanStartingTopInset + translation.y + } + owner.bottomSheetView.snp.updateConstraints { + $0.top.equalTo(owner.view.safeAreaLayoutGuide) + .inset(owner.bottomSheetViewTopInset) + } + + case .ended: + let nearestValue = owner.nearest( + to: owner.bottomSheetViewTopInset, + inValues: [defaultPadding, owner.maxTopInset] + ) + + if nearestValue == defaultPadding { + owner.showBottomSheet(atState: owner.state) + } else { + owner.dismissBottomSheet() + } + + default: + return + } + }) + .disposed(by: disposeBag) + } + + public func configureNavigation() {} +} + +extension BaseBottomSheetViewController { + private func showBottomSheet(atState: BottomSheetViewState) { + switch atState { + case .normal: + bottomSheetViewTopInset = maxTopInset - defaultHeight - dragHeight + case let .custom(customHegiht): + defaultHeight = customHegiht + let topInset = maxTopInset - customHegiht - dragHeight + if topInset > 0 { + bottomSheetViewTopInset = topInset + } else { + bottomSheetViewTopInset = bottomSheetPanMinTopInset + defaultHeight = maxTopInset - bottomSheetPanMinTopInset + } + } + + bottomSheetView.snp.remakeConstraints { + $0.leading.trailing.bottom.equalToSuperview() + $0.top.equalTo(view.safeAreaLayoutGuide).inset(bottomSheetViewTopInset) + } + + UIView.animate( + withDuration: 0.2, + delay: 0, + options: .curveEaseInOut + ) { self.dimmedView.alpha = 1 } + + UIView.animate( + withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 0.76, + initialSpringVelocity: 0.0 + ) { self.view.layoutIfNeeded() } + } + + public func dismissBottomSheet() { + bottomSheetViewTopInset = maxTopInset + bottomSheetView.snp.remakeConstraints { + $0.leading.trailing.bottom.equalToSuperview() + $0.top.equalTo(view.safeAreaLayoutGuide).inset(bottomSheetViewTopInset) + } + + UIView.animate( + withDuration: 0.2, + delay: 0, + options: .curveEaseInOut + ) { + self.dimmedView.alpha = 0 + self.view.layoutIfNeeded() + } completion: { _ in + self.dismiss(animated: false) + } + } + + // 가까이 있는 숫자 반환해주는 함수 + private func nearest(to number: CGFloat, inValues values: [CGFloat]) -> CGFloat { + guard let nearestVal = values.min(by: { abs(number - $0) < abs(number - $1) }) + else { return number } + return nearestVal + } +} diff --git a/Projects/Presentation/Sources/Base/BaseTabBarController.swift b/Projects/Presentation/Sources/Base/BaseTabBarController.swift index bdd31960..eae294bb 100644 --- a/Projects/Presentation/Sources/Base/BaseTabBarController.swift +++ b/Projects/Presentation/Sources/Base/BaseTabBarController.swift @@ -9,7 +9,6 @@ public class BaseTabBarController: UITabBarController, private let stroke = UIView().then { $0.backgroundColor = .GrayScale.gray30 } - private let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) public override func viewDidLoad() { super.viewDidLoad() @@ -33,10 +32,6 @@ public class BaseTabBarController: UITabBarController, $0.height.equalTo(1) } } - - public override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { - self.impactFeedbackGenerator.impactOccurred() - } } extension BaseTabBarController: UITabBarControllerDelegate { diff --git a/Projects/Presentation/Sources/DI/PresentationAssembly.swift b/Projects/Presentation/Sources/DI/PresentationAssembly.swift index 771ff1fb..8cba348a 100644 --- a/Projects/Presentation/Sources/DI/PresentationAssembly.swift +++ b/Projects/Presentation/Sources/DI/PresentationAssembly.swift @@ -31,8 +31,12 @@ public final class PresentationAssembly: Assembly { container.register(RecruitmentViewController.self) { resolver in RecruitmentViewController(resolver.resolve(RecruitmentViewModel.self)!) } - container.register(RecruitmentViewModel.self) { _ in - RecruitmentViewModel() + + container.register(RecruitmentViewModel.self) { resolver in + RecruitmentViewModel( + fetchRecruitmentListUseCase: resolver.resolve(FetchRecruitmentListUseCase.self)!, + bookmarkUseCase: resolver.resolve(BookmarkUseCase.self)! + ) } container.register(BookmarkViewController.self) { resolver in diff --git a/Projects/Presentation/Sources/MyPage/Components/Cell/SectionTableViewCell.swift b/Projects/Presentation/Sources/MyPage/Components/Cell/SectionTableViewCell.swift index 8f9aaea8..374c28a0 100644 --- a/Projects/Presentation/Sources/MyPage/Components/Cell/SectionTableViewCell.swift +++ b/Projects/Presentation/Sources/MyPage/Components/Cell/SectionTableViewCell.swift @@ -1,22 +1,25 @@ import UIKit import DesignSystem -final class SectionTableViewCell: UITableViewCell { +typealias SectionType = (String, UIImage) +final class SectionTableViewCell: BaseTableViewCell { static let identifier = "SectionTableViewCell" private let sectionImageView = UIImageView() private let titleLabel = UILabel() override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) - self.titleLabel.textColor = self.isHighlighted ? .GrayScale.gray90.withAlphaComponent(0.1) : .GrayScale.gray90 + self.titleLabel.textColor = self.isHighlighted ? .GrayScale.gray50 : .GrayScale.gray90 } - override func layoutSubviews() { + override func addView() { [ sectionImageView, titleLabel ].forEach { self.addSubview($0) } + } + override func setLayout() { sectionImageView.snp.makeConstraints { $0.top.bottom.equalToSuperview().inset(12) $0.width.equalTo(28) @@ -28,9 +31,9 @@ final class SectionTableViewCell: UITableViewCell { } } - func setCell(image: UIImage, title: String) { - self.sectionImageView.image = image - self.titleLabel.setJobisText(title, font: .body, color: .GrayScale.gray90) + override func adapt(model: SectionType) { + self.sectionImageView.image = model.1 + self.titleLabel.setJobisText(model.0, font: .body, color: .GrayScale.gray90) self.selectionStyle = .none } } diff --git a/Projects/Presentation/Sources/MyPage/Components/HelpSectionView.swift b/Projects/Presentation/Sources/MyPage/Components/HelpSectionView.swift new file mode 100644 index 00000000..24f794cc --- /dev/null +++ b/Projects/Presentation/Sources/MyPage/Components/HelpSectionView.swift @@ -0,0 +1,32 @@ +import UIKit +import SnapKit +import Then +import RxSwift +import RxCocoa +import DesignSystem + +final class HelpSectionView: BaseView { + enum HelpSectionType: Int { + case announcement + } + private let helpSectionView = SectionView( + menuText: "도움말", + items: [ + ("공지사항", .jobisIcon(.sound)) + ] + ) + + override func addView() { + self.addSubview(helpSectionView) + } + + override func setLayout() { + helpSectionView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + func getSelectedItem(type: HelpSectionType) -> Observable { + self.helpSectionView.getSelectedItem(index: type.rawValue) + } +} diff --git a/Projects/Presentation/Sources/MyPage/Components/SectionView.swift b/Projects/Presentation/Sources/MyPage/Components/SectionView.swift index 7b4322f3..b2e9d5ec 100644 --- a/Projects/Presentation/Sources/MyPage/Components/SectionView.swift +++ b/Projects/Presentation/Sources/MyPage/Components/SectionView.swift @@ -64,7 +64,8 @@ extension SectionView: UITableViewDataSource { withIdentifier: SectionTableViewCell.identifier, for: indexPath ) as? SectionTableViewCell else { return UITableViewCell() } - cell.setCell(image: items[indexPath.row].icon, title: items[indexPath.row].title) + cell.adapt(model: items[indexPath.row]) + return cell } } diff --git a/Projects/Presentation/Sources/MyPage/MyPageViewController.swift b/Projects/Presentation/Sources/MyPage/MyPageViewController.swift index 9c52cfc0..259c1ff4 100644 --- a/Projects/Presentation/Sources/MyPage/MyPageViewController.swift +++ b/Projects/Presentation/Sources/MyPage/MyPageViewController.swift @@ -19,6 +19,7 @@ public final class MyPageViewController: BaseViewController { private let reviewNavigateStackView = ReviewNavigateStackView() private let accountSectionView = AccountSectionView() private let bugSectionView = BugSectionView() + private let helpSectionView = HelpSectionView() public override func addView() { self.view.addSubview(scrollView) @@ -27,6 +28,7 @@ public final class MyPageViewController: BaseViewController { studentInfoView, editButton, reviewNavigateStackView, + helpSectionView, accountSectionView, bugSectionView ].forEach { self.contentView.addSubview($0) } @@ -36,31 +38,43 @@ public final class MyPageViewController: BaseViewController { scrollView.snp.makeConstraints { $0.edges.equalToSuperview() } + contentView.snp.makeConstraints { $0.edges.equalTo(scrollView.contentLayoutGuide) $0.top.width.equalToSuperview() - $0.bottom.equalTo(bugSectionView) + $0.bottom.equalTo(bugSectionView).offset(60) } + studentInfoView.snp.makeConstraints { $0.top.equalToSuperview() $0.leading.trailing.equalToSuperview() } + editButton.snp.makeConstraints { $0.centerY.equalTo(studentInfoView) $0.trailing.equalToSuperview().offset(-28) } + reviewNavigateStackView.snp.updateConstraints { $0.leading.trailing.equalToSuperview().inset(24) $0.top.equalTo(studentInfoView.snp.bottom) } - accountSectionView.snp.makeConstraints { + + helpSectionView.snp.makeConstraints { $0.top.equalTo(reviewNavigateStackView.snp.bottom) $0.leading.trailing.equalToSuperview() } + + accountSectionView.snp.makeConstraints { + $0.top.equalTo(helpSectionView.snp.bottom) + $0.leading.trailing.equalToSuperview() + } + bugSectionView.snp.makeConstraints { $0.top.equalTo(accountSectionView.snp.bottom) $0.leading.trailing.equalToSuperview() } + } public override func bind() { diff --git a/Projects/Presentation/Sources/Recruitment/RecruitmentTableViewCell.swift b/Projects/Presentation/Sources/Recruitment/Cell/RecruitmentTableViewCell.swift similarity index 54% rename from Projects/Presentation/Sources/Recruitment/RecruitmentTableViewCell.swift rename to Projects/Presentation/Sources/Recruitment/Cell/RecruitmentTableViewCell.swift index 7b4a553c..cb02fabb 100644 --- a/Projects/Presentation/Sources/Recruitment/RecruitmentTableViewCell.swift +++ b/Projects/Presentation/Sources/Recruitment/Cell/RecruitmentTableViewCell.swift @@ -1,18 +1,30 @@ import UIKit +import Domain import DesignSystem import SnapKit import Then import RxSwift import RxCocoa -final class RecruitmentTableViewCell: UITableViewCell { +final class RecruitmentTableViewCell: BaseTableViewCell { static let identifier = "RecruitmentTableViewCell" - + public var bookmarkButtonDidTap: (()->(Void))? + public var recruitmentID = 0 private var disposeBag = DisposeBag() - private var isBookmarked: Bool = false + private var isBookmarked = false { + didSet { + var bookmarkImage: JobisIcon { + isBookmarked ? .bookmarkOn: .bookmarkOff + } + bookmarkButton.setImage( + .jobisIcon(bookmarkImage) + .resize(size: 28), for: .normal + ) + } + } private let companyProfileImageView = UIImageView().then { - $0.backgroundColor = .blue $0.layer.cornerRadius = 8 + $0.clipsToBounds = true } private let fieldTypeLabel = UILabel().then { $0.setJobisText( @@ -20,6 +32,8 @@ final class RecruitmentTableViewCell: UITableViewCell { font: .subHeadLine, color: UIColor.GrayScale.gray90 ) + $0.numberOfLines = 1 + $0.lineBreakMode = .byTruncatingTail } private let benefitsLabel = UILabel().then { $0.setJobisText( @@ -35,46 +49,11 @@ final class RecruitmentTableViewCell: UITableViewCell { color: UIColor.GrayScale.gray70 ) } - private let bookmarkButton = UIButton().then { - $0.setImage( - .jobisIcon(.bookmarkOff).resize(size: 28), - for: .normal - ) - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - backgroundColor = UIColor.GrayScale.gray10 - addView() - layout() - attribute() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + public let bookmarkButton = UIButton().then { + $0.setImage(.jobisIcon(.bookmarkOff).resize(size: 28), for: .normal) } - private func attribute() { - bookmarkButton.rx.tap.asObservable() - .subscribe(onNext: { [weak self] in - guard let self else { return } - bookmark() - }) - .disposed(by: disposeBag) - } - - private func bookmark() { - var bookmarkImage: JobisIcon { - isBookmarked ? .bookmarkOn: .bookmarkOff - } - bookmarkButton.setImage( - .jobisIcon(bookmarkImage).resize(size: 28), - for: .normal - ) - isBookmarked.toggle() - } - - private func addView() { + override func addView() { [ companyProfileImageView, fieldTypeLabel, @@ -86,15 +65,20 @@ final class RecruitmentTableViewCell: UITableViewCell { } } - private func layout() { + override func setLayout() { companyProfileImageView.snp.makeConstraints { $0.top.equalToSuperview().inset(12) $0.left.equalToSuperview().inset(24) $0.width.height.equalTo(48) } + bookmarkButton.snp.makeConstraints { + $0.top.equalToSuperview().inset(12) + $0.right.equalToSuperview().inset(24) + } fieldTypeLabel.snp.makeConstraints { $0.top.equalToSuperview().inset(12) $0.left.equalTo(companyProfileImageView.snp.right).offset(12) + $0.right.equalToSuperview().inset(52) } benefitsLabel.snp.makeConstraints { $0.top.equalTo(fieldTypeLabel.snp.bottom).offset(4) @@ -104,9 +88,39 @@ final class RecruitmentTableViewCell: UITableViewCell { $0.top.equalTo(benefitsLabel.snp.bottom).offset(4) $0.left.equalTo(companyProfileImageView.snp.right).offset(12) } - bookmarkButton.snp.makeConstraints { - $0.top.equalToSuperview().inset(12) - $0.right.equalToSuperview().inset(24) - } + } + + override func configureView() { + companyProfileImageView.layer.cornerRadius = 8 + bookmarkButton.rx.tap + .bind(onNext: { [weak self] in + self?.bookmarkButtonDidTap?() + self?.isBookmarked.toggle() + }) + .disposed(by: disposeBag) + } + + override func adapt(model: RecruitmentEntity) { + companyProfileImageView.setJobisImage( + urlString: model.companyProfileURL + ) + fieldTypeLabel.setJobisText( + model.hiringJobs, + font: .subHeadLine, + color: .GrayScale.gray90 + ) + let militarySupport = model.militarySupport ? "O": "X" + benefitsLabel.setJobisText( + "병역특례 \(militarySupport) · 실습 수당 \(model.trainPay)만원", + font: .subBody, + color: .GrayScale.gray70 + ) + companyLabel.setJobisText( + model.companyName, + font: .description, + color: .GrayScale.gray70 + ) + recruitmentID = model.recruitID + isBookmarked = model.bookmarked } } diff --git a/Projects/Presentation/Sources/Recruitment/RecruitmentViewController.swift b/Projects/Presentation/Sources/Recruitment/RecruitmentViewController.swift index 45f8d45b..508336f9 100644 --- a/Projects/Presentation/Sources/Recruitment/RecruitmentViewController.swift +++ b/Projects/Presentation/Sources/Recruitment/RecruitmentViewController.swift @@ -1,4 +1,5 @@ import UIKit +import Domain import RxSwift import RxCocoa import SnapKit @@ -7,75 +8,82 @@ import Core import DesignSystem public final class RecruitmentViewController: BaseViewController { - private let tableView = UITableView().then { - $0.register(RecruitmentTableViewCell.self, forCellReuseIdentifier: RecruitmentTableViewCell.identifier) + private let bookmarkButtonDidClicked = PublishRelay() + private let pageCount = PublishRelay() + private let recruitmentTableView = UITableView().then { + $0.register( + RecruitmentTableViewCell.self, + forCellReuseIdentifier: RecruitmentTableViewCell.identifier + ) $0.separatorStyle = .none $0.rowHeight = 96 + $0.showsVerticalScrollIndicator = false } - private let navigateToFilterButton = UIButton().then { + private let filterButton = UIButton().then { $0.setImage(.jobisIcon(.filterIcon), for: .normal) } - private let navigateToSearchButton = UIButton().then { + private let searchButton = UIButton().then { $0.setImage(.jobisIcon(.searchIcon), for: .normal) } public override func addView() { - self.view.addSubview(tableView) + self.view.addSubview(recruitmentTableView) } public override func setLayout() { - tableView.snp.makeConstraints { + recruitmentTableView.snp.makeConstraints { $0.edges.equalToSuperview() } } - public override func configureViewController() { - tableView.dataSource = self - tableView.delegate = self + public override func bind() { + let input = RecruitmentViewModel.Input( + viewAppear: self.viewWillAppearPublisher, + bookMarkButtonDidTap: bookmarkButtonDidClicked, + pageChange: pageCount + ) + + let output = viewModel.transform(input) - navigateToSearchButton.rx.tap - .subscribe(onNext: { _ in - print("hello") - }) + output.recruitmentData + .bind( + to: recruitmentTableView.rx.items( + cellIdentifier: RecruitmentTableViewCell.identifier, + cellType: RecruitmentTableViewCell.self + )) { _, element, cell in + cell.adapt(model: element) + cell.bookmarkButtonDidTap = { + self.bookmarkButtonDidClicked.accept(cell.recruitmentID) + } + } + .disposed(by: disposeBag) + } + + public override func configureViewController() { + recruitmentTableView.delegate = self + searchButton.rx.tap + .subscribe(onNext: { _ in }) .disposed(by: disposeBag) } public override func configureNavigation() { navigationItem.rightBarButtonItems = [ - UIBarButtonItem(customView: navigateToFilterButton), - UIBarButtonItem(customView: navigateToSearchButton) + UIBarButtonItem(customView: filterButton), + UIBarButtonItem(customView: searchButton) ] setLargeTitle(title: "모집의뢰서") } } -extension RecruitmentViewController: UITableViewDataSource, UITableViewDelegate { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 10 - } - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell( - withIdentifier: RecruitmentTableViewCell.identifier, - for: indexPath - ) as? RecruitmentTableViewCell else { return UITableViewCell() } - - return cell - } - - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let cell = tableView.cellForRow(at: indexPath) { - cell.contentView.backgroundColor = UIColor.GrayScale.gray20 - // 다른 동작 수행 가능 - // 예: 특정 셀을 선택했을 때의 동작 처리 - - // 지연 작업을 통해 일정 시간 후에 원래 색으로 돌아가도록 함 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - UIView.animate(withDuration: 0.1) { - cell.contentView.backgroundColor = UIColor.GrayScale.gray10 - } - } +extension RecruitmentViewController: UITableViewDelegate { + public func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + let lastRowIndex = tableView.numberOfRows(inSection: indexPath.section) - 1 + if indexPath.row == lastRowIndex { + pageCount.accept(indexPath.row) } - - tableView.deselectRow(at: indexPath, animated: false) } } diff --git a/Projects/Presentation/Sources/Recruitment/RecruitmentViewModel.swift b/Projects/Presentation/Sources/Recruitment/RecruitmentViewModel.swift index 139c258c..00468963 100644 --- a/Projects/Presentation/Sources/Recruitment/RecruitmentViewModel.swift +++ b/Projects/Presentation/Sources/Recruitment/RecruitmentViewModel.swift @@ -6,17 +6,68 @@ import Core import Domain public final class RecruitmentViewModel: BaseViewModel, Stepper { - public var steps = PublishRelay() - + public let steps = PublishRelay() private let disposeBag = DisposeBag() + private let fetchRecruitmentListUseCase: FetchRecruitmentListUseCase + private let bookmarkUseCase: BookmarkUseCase + private var recruitmentData = BehaviorRelay<[RecruitmentEntity]>(value: []) + private var pageCount: Int = 1 + + init( + fetchRecruitmentListUseCase: FetchRecruitmentListUseCase, + bookmarkUseCase: BookmarkUseCase + ) { + self.fetchRecruitmentListUseCase = fetchRecruitmentListUseCase + self.bookmarkUseCase = bookmarkUseCase + } public struct Input { + let viewAppear: PublishRelay + let bookMarkButtonDidTap: PublishRelay + var pageChange: PublishRelay } public struct Output { + var recruitmentData = BehaviorRelay<[RecruitmentEntity]>(value: []) } public func transform(_ input: Input) -> Output { - return Output() + input.viewAppear.asObservable() + .flatMap { + self.pageCount = 1 + return self.fetchRecruitmentListUseCase.execute(page: self.pageCount) + } + .bind(onNext: { + self.recruitmentData.accept([]) + var currentElements = self.recruitmentData.value + currentElements.append(contentsOf: $0) + self.recruitmentData.accept(currentElements) + }) + .disposed(by: disposeBag) + + input.pageChange.asObservable() + .flatMap { value in + if value == self.recruitmentData.value.count-1 { + self.pageCount += 1 + return self.fetchRecruitmentListUseCase.execute(page: self.pageCount) + } else { + return Single.just([]) + } + } + .bind(onNext: { + var currentElements = self.recruitmentData.value + currentElements.append(contentsOf: $0) + self.recruitmentData.accept(currentElements) + }) + .disposed(by: disposeBag) + + input.bookMarkButtonDidTap.asObservable() + .flatMap { id in + self.bookmarkUseCase.execute(id: id) + }.subscribe(onCompleted: { + print("bookmark!") + }).disposed(by: disposeBag) + + return Output(recruitmentData: recruitmentData) } }