diff --git a/Projects/Core/Sources/Steps/AddReviewStep.swift b/Projects/Core/Sources/Steps/AddReviewStep.swift new file mode 100644 index 00000000..a62ac4ea --- /dev/null +++ b/Projects/Core/Sources/Steps/AddReviewStep.swift @@ -0,0 +1,6 @@ +import RxFlow + +public enum AddReviewStep: Step { + case addReviewIsRequired + case dismissToWritableReview +} diff --git a/Projects/Core/Sources/Steps/CompanyDetailStep.swift b/Projects/Core/Sources/Steps/CompanyDetailStep.swift index 95cbf90d..fa720681 100644 --- a/Projects/Core/Sources/Steps/CompanyDetailStep.swift +++ b/Projects/Core/Sources/Steps/CompanyDetailStep.swift @@ -4,4 +4,8 @@ public enum CompanyDetailStep: Step { case companyDetailIsRequired case popIsRequired case recruitmentDetailIsRequired(id: Int) + case interviewReviewDetailIsRequired( + id: Int, + name: String + ) } diff --git a/Projects/Core/Sources/Steps/InterviewReviewStep.swift b/Projects/Core/Sources/Steps/InterviewReviewStep.swift new file mode 100644 index 00000000..50795957 --- /dev/null +++ b/Projects/Core/Sources/Steps/InterviewReviewStep.swift @@ -0,0 +1,5 @@ +import RxFlow + +public enum InterviewReviewDetailStep: Step { + case interviewReviewDetailIsRequired +} diff --git a/Projects/Core/Sources/Steps/MyPageStep.swift b/Projects/Core/Sources/Steps/MyPageStep.swift index 714c29f4..ec563822 100644 --- a/Projects/Core/Sources/Steps/MyPageStep.swift +++ b/Projects/Core/Sources/Steps/MyPageStep.swift @@ -3,6 +3,7 @@ import RxFlow public enum MyPageStep: Step { case myPageIsRequired case tabsIsRequired + case writableReviewIsRequired(_ id: Int) case noticeIsRequired case confirmIsRequired } diff --git a/Projects/Core/Sources/Steps/WritableReviewStep.swift b/Projects/Core/Sources/Steps/WritableReviewStep.swift new file mode 100644 index 00000000..33d608d0 --- /dev/null +++ b/Projects/Core/Sources/Steps/WritableReviewStep.swift @@ -0,0 +1,7 @@ +import RxFlow + +public enum WritableReviewStep: Step { + case writableReviewIsRequired + case addReviewIsRequired + case popToMyPage +} diff --git a/Projects/Data/Sources/DTO/Companies/WritableReviewListResponseDTO.swift b/Projects/Data/Sources/DTO/Companies/WritableReviewListResponseDTO.swift index 6f827e5d..3374f94f 100644 --- a/Projects/Data/Sources/DTO/Companies/WritableReviewListResponseDTO.swift +++ b/Projects/Data/Sources/DTO/Companies/WritableReviewListResponseDTO.swift @@ -6,11 +6,11 @@ struct WritableReviewListResponseDTO: Decodable { } struct WritableReviewCompanyResponseDTO: Decodable { - let reviewID: Int + let companyID: Int let name: String enum CodingKeys: String, CodingKey { - case reviewID = "id" + case companyID = "id" case name } } @@ -18,7 +18,7 @@ struct WritableReviewCompanyResponseDTO: Decodable { extension WritableReviewListResponseDTO { func toDomain() -> [WritableReviewCompanyEntity] { companies.map { - WritableReviewCompanyEntity(reviewID: $0.reviewID, name: $0.name) + WritableReviewCompanyEntity(companyID: $0.companyID, name: $0.name) } } } diff --git a/Projects/Domain/Sources/Entities/Companies/WritableReviewCompanyEntity.swift b/Projects/Domain/Sources/Entities/Reviews/WritableReviewCompanyEntity.swift similarity index 56% rename from Projects/Domain/Sources/Entities/Companies/WritableReviewCompanyEntity.swift rename to Projects/Domain/Sources/Entities/Reviews/WritableReviewCompanyEntity.swift index bea7a9be..06ed7925 100644 --- a/Projects/Domain/Sources/Entities/Companies/WritableReviewCompanyEntity.swift +++ b/Projects/Domain/Sources/Entities/Reviews/WritableReviewCompanyEntity.swift @@ -1,11 +1,11 @@ import Foundation public struct WritableReviewCompanyEntity: Equatable, Hashable { - public let reviewID: Int + public let companyID: Int public let name: String - public init(reviewID: Int, name: String) { - self.reviewID = reviewID + public init(companyID: Int, name: String) { + self.companyID = companyID self.name = name } } diff --git a/Projects/Flow/Sources/Company/CompanyDetailFlow.swift b/Projects/Flow/Sources/Company/CompanyDetailFlow.swift index 4288cb1b..373fdbc6 100644 --- a/Projects/Flow/Sources/Company/CompanyDetailFlow.swift +++ b/Projects/Flow/Sources/Company/CompanyDetailFlow.swift @@ -28,6 +28,9 @@ public final class CompanyDetailFlow: Flow { case let.recruitmentDetailIsRequired(id): return navigateToRecruimtentDetail(recruitmentID: id) + + case let .interviewReviewDetailIsRequired(id, name): + return navigateToInterviewReviewDetail(id, name) } } } @@ -62,4 +65,22 @@ private extension CompanyDetailFlow { withNextStepper: OneStepper(withSingleStep: RecruitmentDetailStep.recruitmentDetailIsRequired) )) } + + func navigateToInterviewReviewDetail(_ id: Int, _ name: String) -> FlowContributors { + let interviewReviewDetailFlow = InterviewReviewDetailFlow(container: container) + + Flows.use(interviewReviewDetailFlow, when: .created) { (root) in + let view = root as? InterviewReviewDetailViewController + view?.viewModel.reviewId = id + view?.viewModel.writerName = name + self.rootViewController.navigationController?.pushViewController( + view!, animated: true + ) + } + + return .one(flowContributor: .contribute( + withNextPresentable: interviewReviewDetailFlow, + withNextStepper: OneStepper(withSingleStep: InterviewReviewDetailStep.interviewReviewDetailIsRequired) + )) + } } diff --git a/Projects/Flow/Sources/Company/InterviewReviewFlow.swift b/Projects/Flow/Sources/Company/InterviewReviewFlow.swift new file mode 100644 index 00000000..7ea9063f --- /dev/null +++ b/Projects/Flow/Sources/Company/InterviewReviewFlow.swift @@ -0,0 +1,36 @@ +import UIKit +import Presentation +import Swinject +import RxFlow +import Core + +public final class InterviewReviewDetailFlow: Flow { + public let container: Container + private let rootViewController: InterviewReviewDetailViewController + public var root: Presentable { + return rootViewController + } + + public init(container: Container) { + self.container = container + self.rootViewController = container.resolve(InterviewReviewDetailViewController.self)! + } + + public func navigate(to step: Step) -> FlowContributors { + guard let step = step as? InterviewReviewDetailStep else { return .none } + + switch step { + case .interviewReviewDetailIsRequired: + return navigateToInterviewReviewDetail() + } + } +} + +private extension InterviewReviewDetailFlow { + func navigateToInterviewReviewDetail() -> FlowContributors { + return .one(flowContributor: .contribute( + withNextPresentable: rootViewController, + withNextStepper: rootViewController.viewModel + )) + } +} diff --git a/Projects/Flow/Sources/MyPage/MyPageFlow.swift b/Projects/Flow/Sources/MyPage/MyPageFlow.swift index 12c08901..00b767d5 100644 --- a/Projects/Flow/Sources/MyPage/MyPageFlow.swift +++ b/Projects/Flow/Sources/MyPage/MyPageFlow.swift @@ -25,6 +25,9 @@ public final class MyPageFlow: Flow { case .tabsIsRequired: return .end(forwardToParentFlowWithStep: TabsStep.appIsRequired) + case let .writableReviewIsRequired(id): + return navigateToWritableReview(id) + case .noticeIsRequired: return navigateToNotice() @@ -49,6 +52,23 @@ private extension MyPageFlow { )) } + func navigateToWritableReview(_ id: Int) -> FlowContributors { + let writableReviewFlow = WritableReviewFlow(container: container) + + Flows.use(writableReviewFlow, when: .created) { (root) in + let view = root as? WritableReviewViewController + view?.viewModel.companyID = id + self.rootViewController.pushViewController( + view!, animated: true + ) + } + + return .one(flowContributor: .contribute( + withNextPresentable: writableReviewFlow, + withNextStepper: OneStepper(withSingleStep: WritableReviewStep.writableReviewIsRequired) + )) + } + func navigateToNotice() -> FlowContributors { let noticeFlow = NoticeFlow(container: container) diff --git a/Projects/Flow/Sources/MyPage/Review/AddReviewFlow.swift b/Projects/Flow/Sources/MyPage/Review/AddReviewFlow.swift new file mode 100644 index 00000000..a17221e9 --- /dev/null +++ b/Projects/Flow/Sources/MyPage/Review/AddReviewFlow.swift @@ -0,0 +1,48 @@ +import UIKit +import Presentation +import Swinject +import RxFlow +import Core + +public final class AddReviewFlow: Flow { + public let container: Container + private let rootViewController: AddReviewViewController + public var root: Presentable { + return rootViewController + } + + public init(container: Container) { + self.container = container + self.rootViewController = AddReviewViewController( + container.resolve(AddReviewViewModel.self)!, + state: .custom(height: 500) + ) + } + + public func navigate(to step: Step) -> FlowContributors { + guard let step = step as? AddReviewStep else { return .none } + + switch step { + case .addReviewIsRequired: + return navigateToAddReview() + + case .dismissToWritableReview: + return dismissToWritableReview() + } + } +} + +private extension AddReviewFlow { + func navigateToAddReview() -> FlowContributors { + return .one(flowContributor: .contribute( + withNextPresentable: rootViewController, + withNextStepper: rootViewController.viewModel + )) + } + + func dismissToWritableReview( + ) -> FlowContributors { + self.rootViewController.dismissBottomSheet() + return .none + } +} diff --git a/Projects/Flow/Sources/MyPage/Review/WritableReviewFlow.swift b/Projects/Flow/Sources/MyPage/Review/WritableReviewFlow.swift new file mode 100644 index 00000000..84dbe2b4 --- /dev/null +++ b/Projects/Flow/Sources/MyPage/Review/WritableReviewFlow.swift @@ -0,0 +1,76 @@ +import UIKit +import Presentation +import Swinject +import RxFlow +import Core +import Domain + +public final class WritableReviewFlow: Flow { + public let container: Container + private let rootViewController: WritableReviewViewController + public var root: Presentable { + return rootViewController + } + + public init(container: Container) { + self.container = container + self.rootViewController = container.resolve(WritableReviewViewController.self)! + } + + public func navigate(to step: Step) -> FlowContributors { + guard let step = step as? WritableReviewStep else { return .none } + + switch step { + case .writableReviewIsRequired: + return navigateToWritableReview() + + case .addReviewIsRequired: + return navigateToAddReview() + + case .popToMyPage: + return popToMyPage() + } + } +} + +private extension WritableReviewFlow { + func navigateToWritableReview() -> FlowContributors { + return .one(flowContributor: .contribute( + withNextPresentable: rootViewController, + withNextStepper: rootViewController.viewModel + )) + } + + func navigateToAddReview() -> FlowContributors { + let addReviewFlow = AddReviewFlow(container: container) + Flows.use(addReviewFlow, when: .created) { root in + let view = root as? AddReviewViewController + view?.dismiss = { question, answer, techCode in + self.rootViewController.viewModel.techCode = techCode.code + self.rootViewController.viewModel.interviewReviewInfo.accept( + QnaEntity( + question: question, + answer: answer, + area: techCode.keyword + ) + ) + } + self.rootViewController.present( + root, + animated: false + ) + } + + return .one(flowContributor: .contribute( + withNextPresentable: addReviewFlow, + withNextStepper: OneStepper( + withSingleStep: AddReviewStep.addReviewIsRequired + ) + )) + } + + func popToMyPage() -> FlowContributors { + self.rootViewController.navigationController?.popViewController(animated: true) + return .none + } +} diff --git a/Projects/Modules/DesignSystem/Sources/Extensions/UITextField/UITextField+addPadding.swift b/Projects/Modules/DesignSystem/Sources/Extensions/UITextField/UITextField+addPadding.swift index 89b7e0b9..8cc4faf2 100644 --- a/Projects/Modules/DesignSystem/Sources/Extensions/UITextField/UITextField+addPadding.swift +++ b/Projects/Modules/DesignSystem/Sources/Extensions/UITextField/UITextField+addPadding.swift @@ -10,4 +10,14 @@ public extension UITextField { self.rightView = UIView(frame: CGRect(x: 0, y: 0, width: size, height: 0)) self.rightViewMode = .always } + + func setPlaceholderColor(_ placeholderColor: UIColor) { + attributedPlaceholder = NSAttributedString( + string: placeholder ?? "", + attributes: [ + .foregroundColor: placeholderColor, + .font: font + ].compactMapValues { $0 } + ) + } } diff --git a/Projects/Presentation/Sources/CompanyDetail/CompanyDetailViewController.swift b/Projects/Presentation/Sources/CompanyDetail/CompanyDetailViewController.swift index 46bfa13f..2454268b 100644 --- a/Projects/Presentation/Sources/CompanyDetail/CompanyDetailViewController.swift +++ b/Projects/Presentation/Sources/CompanyDetail/CompanyDetailViewController.swift @@ -9,6 +9,7 @@ import DesignSystem public class CompanyDetailViewController: BaseViewController { private let companyDetailProfileView = CompanyDetailProfileView() + public var isTabNavigation: Bool = true private let scrollView = UIScrollView().then { $0.showsVerticalScrollIndicator = false } @@ -118,7 +119,14 @@ public class CompanyDetailViewController: BaseViewController let recruitmentButtonDidTap: Signal + let interviewReviewTableViewDidTap: Observable<(Int, String)> } public struct Output { @@ -64,6 +65,12 @@ public final class CompanyDetailViewModel: BaseViewModel, Stepper { .bind(to: steps) .disposed(by: disposeBag) + input.interviewReviewTableViewDidTap.asObservable() + .map { id, name in + CompanyDetailStep.interviewReviewDetailIsRequired(id: id, name: name) + } + .bind(to: steps) + .disposed(by: disposeBag) return Output( companyDetailInfo: companyDetailInfo, reviewListInfo: reviewListInfo diff --git a/Projects/Presentation/Sources/DI/PresentationAssembly.swift b/Projects/Presentation/Sources/DI/PresentationAssembly.swift index df2a391f..afe912aa 100644 --- a/Projects/Presentation/Sources/DI/PresentationAssembly.swift +++ b/Projects/Presentation/Sources/DI/PresentationAssembly.swift @@ -148,6 +148,22 @@ public final class PresentationAssembly: Assembly { RenewalPasswordViewController(resolver.resolve(RenewalPasswordViewModel.self)!) } + container.register(WritableReviewViewModel.self) { resolver in + WritableReviewViewModel( + postReviewUseCase: resolver.resolve(PostReviewUseCase.self)! + ) + } + + container.register(WritableReviewViewController.self) { resolver in + WritableReviewViewController(resolver.resolve(WritableReviewViewModel.self)!) + } + + container.register(AddReviewViewModel.self) { resolver in + AddReviewViewModel( + fetchCodeListUseCase: resolver.resolve(FetchCodeListUseCase.self)! + ) + } + container.register(NoticeViewModel.self) { resolver in NoticeViewModel( fetchNoticeListUseCase: resolver.resolve(FetchNoticeListUseCase.self)! @@ -194,6 +210,15 @@ public final class PresentationAssembly: Assembly { CompanyDetailViewController(resolver.resolve(CompanyDetailViewModel.self)!) } + container.register(InterviewReviewDetailViewModel.self) { resolver in + InterviewReviewDetailViewModel( + fetchReviewDetailUseCase: resolver.resolve(FetchReviewDetailUseCase.self)! + ) + } + container.register(InterviewReviewDetailViewController.self) { resolver in + InterviewReviewDetailViewController(resolver.resolve(InterviewReviewDetailViewModel.self)!) + } + container.register(RecruitmentDetailViewModel.self) { resolver in RecruitmentDetailViewModel( fetchRecruitmentDetailUseCase: resolver.resolve(FetchRecruitmentDetailUseCase.self)!, diff --git a/Projects/Presentation/Sources/InterviewReview/InterviewReviewViewController.swift b/Projects/Presentation/Sources/InterviewReview/InterviewReviewViewController.swift new file mode 100644 index 00000000..110f39a5 --- /dev/null +++ b/Projects/Presentation/Sources/InterviewReview/InterviewReviewViewController.swift @@ -0,0 +1,99 @@ +import UIKit +import Domain +import RxSwift +import RxCocoa +import SnapKit +import Then +import Core +import DesignSystem + +public final class InterviewReviewDetailViewController: BaseViewController { + private let scrollView = UIScrollView().then { + $0.showsVerticalScrollIndicator = false + } + private let contentView = UIView() + private let pageTitleLabel = UILabel().then { + $0.setJobisText( + "- 님의 면접 후기", + font: .pageTitle, + color: .GrayScale.gray90 + ) + $0.numberOfLines = 2 + } + private let interviewReviewQuestionLabel = JobisMenuLabel(text: "받은 면접 질문") + private let mainStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 4 + $0.layoutMargins = .init(top: 0, left: 24, bottom: 0, right: 24) + $0.isLayoutMarginsRelativeArrangement = true + } + private let questionListDetailStackView = QuestionListDetailStackView() + + public override func addView() { + [ + questionListDetailStackView + ].forEach { mainStackView.addArrangedSubview($0) } + + [ + pageTitleLabel, + interviewReviewQuestionLabel, + scrollView + ].forEach { view.addSubview($0) } + scrollView.addSubview(contentView) + contentView.addSubview(mainStackView) + } + + public override func setLayout() { + pageTitleLabel.snp.makeConstraints { + $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).inset(20) + $0.leading.trailing.equalToSuperview().inset(24) + } + + interviewReviewQuestionLabel.snp.makeConstraints { + $0.top.equalTo(pageTitleLabel.snp.bottom).offset(20) + $0.leading.equalToSuperview() + } + + scrollView.snp.makeConstraints { + $0.top.equalTo(interviewReviewQuestionLabel.snp.bottom) + $0.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide) + } + + contentView.snp.makeConstraints { + $0.edges.equalTo(scrollView.contentLayoutGuide) + $0.width.equalToSuperview() + $0.bottom.equalTo(mainStackView.snp.bottom).offset(20) + } + + mainStackView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + } + } + + public override func bind() { + let input = InterviewReviewDetailViewModel.Input( + viewWillAppear: self.viewWillAppearPublisher) + + let output = viewModel.transform(input) + + output.qnaListEntity.asObservable() + .bind { + self.questionListDetailStackView.setFieldType($0) + } + .disposed(by: disposeBag) + + self.pageTitleLabel.text = "\(output.writerName)님의 면접 후기" + } + + public override func configureViewController() { + self.viewWillAppearPublisher.asObservable() + .subscribe(onNext: { + self.hideTabbar() + }) + .disposed(by: disposeBag) + } + + public override func configureNavigation() { + self.navigationController?.navigationBar.prefersLargeTitles = false + } +} diff --git a/Projects/Presentation/Sources/InterviewReview/InterviewReviewViewModel.swift b/Projects/Presentation/Sources/InterviewReview/InterviewReviewViewModel.swift new file mode 100644 index 00000000..c6d9d906 --- /dev/null +++ b/Projects/Presentation/Sources/InterviewReview/InterviewReviewViewModel.swift @@ -0,0 +1,45 @@ +import UIKit +import RxSwift +import RxCocoa +import RxFlow +import Core +import Domain + +public final class InterviewReviewDetailViewModel: BaseViewModel, Stepper { + public let steps = PublishRelay() + private let disposeBag = DisposeBag() + public var reviewId: Int = 0 + public var writerName: String = "" + let fetchReviewDetailUseCase: FetchReviewDetailUseCase + + init( + fetchReviewDetailUseCase: FetchReviewDetailUseCase + ) { + self.fetchReviewDetailUseCase = fetchReviewDetailUseCase + } + + public struct Input { + let viewWillAppear: PublishRelay + } + + public struct Output { + let qnaListEntity: PublishRelay<[QnaEntity]> + let writerName: String + } + + public func transform(_ input: Input) -> Output { + let qnaListEntity = PublishRelay<[QnaEntity]>() + + input.viewWillAppear.asObservable() + .flatMap { + self.fetchReviewDetailUseCase.execute(id: self.reviewId) + } + .bind(to: qnaListEntity) + .disposed(by: disposeBag) + + return Output( + qnaListEntity: qnaListEntity, + writerName: writerName + ) + } +} diff --git a/Projects/Presentation/Sources/MyPage/Components/Cell/ReviewNavigateView.swift b/Projects/Presentation/Sources/MyPage/Components/Cell/ReviewNavigateView.swift index 65de0ba6..2206e963 100644 --- a/Projects/Presentation/Sources/MyPage/Components/Cell/ReviewNavigateView.swift +++ b/Projects/Presentation/Sources/MyPage/Components/Cell/ReviewNavigateView.swift @@ -2,7 +2,7 @@ import UIKit import DesignSystem final class ReviewNavigateView: BaseView { - var reviewID = 0 + var companyID = 0 private let reviewImageView = UIImageView().then { $0.image = .jobisIcon(.door) } diff --git a/Projects/Presentation/Sources/MyPage/Components/ReviewNavigateStackView.swift b/Projects/Presentation/Sources/MyPage/Components/ReviewNavigateStackView.swift index 13f4427c..63c72793 100644 --- a/Projects/Presentation/Sources/MyPage/Components/ReviewNavigateStackView.swift +++ b/Projects/Presentation/Sources/MyPage/Components/ReviewNavigateStackView.swift @@ -29,11 +29,11 @@ final class ReviewNavigateStackView: UIStackView { writableReviewCompanylist.forEach { writableReviewCompany in let reviewNavigateTableViewCell = ReviewNavigateView().then { $0.titleLabel.text = "\(writableReviewCompany.name) 면접 후기를 적어주세요!" - $0.reviewID = writableReviewCompany.reviewID + $0.companyID = writableReviewCompany.companyID } reviewNavigateTableViewCell.reviewNavigateButton.rx.tap .bind(onNext: { [weak self] in - self?.reviewNavigateButtonDidTap.accept(reviewNavigateTableViewCell.reviewID) + self?.reviewNavigateButtonDidTap.accept(reviewNavigateTableViewCell.companyID) }) .disposed(by: disposeBag) self.addArrangedSubview(reviewNavigateTableViewCell) diff --git a/Projects/Presentation/Sources/MyPage/MyPageViewController.swift b/Projects/Presentation/Sources/MyPage/MyPageViewController.swift index 608fee32..f58244bb 100644 --- a/Projects/Presentation/Sources/MyPage/MyPageViewController.swift +++ b/Projects/Presentation/Sources/MyPage/MyPageViewController.swift @@ -17,7 +17,7 @@ public final class MyPageViewController: BaseViewController { // private let editButton = UIButton(type: .system).then { // $0.setJobisText("수정", font: .subHeadLine, color: .Primary.blue20) // } -// private let reviewNavigateStackView = ReviewNavigateStackView() + private let reviewNavigateStackView = ReviewNavigateStackView() private let accountSectionView = AccountSectionView() // private let bugSectionView = BugSectionView() private let helpSectionView = HelpSectionView() @@ -30,7 +30,7 @@ public final class MyPageViewController: BaseViewController { [ studentInfoView, // editButton, -// reviewNavigateStackView, + reviewNavigateStackView, helpSectionView, accountSectionView // bugSectionView @@ -58,13 +58,14 @@ public final class MyPageViewController: BaseViewController { // $0.trailing.equalToSuperview().offset(-28) // } -// reviewNavigateStackView.snp.updateConstraints { -// $0.leading.trailing.equalToSuperview().inset(24) -// $0.top.equalTo(studentInfoView.snp.bottom) -// } + reviewNavigateStackView.snp.updateConstraints { + $0.leading.trailing.equalToSuperview().inset(24) + $0.top.equalTo(studentInfoView.snp.bottom) + } helpSectionView.snp.makeConstraints { - $0.top.equalTo(studentInfoView.snp.bottom) + $0.top.equalTo(reviewNavigateStackView.snp.bottom) +// $0.top.equalTo(studentInfoView.snp.bottom) $0.leading.trailing.equalToSuperview() } @@ -82,7 +83,7 @@ public final class MyPageViewController: BaseViewController { public override func bind() { let input = MyPageViewModel.Input( viewAppear: self.viewDidLoadPublisher, -// reviewNavigate: reviewNavigateStackView.reviewNavigateButtonDidTap, + reviewNavigate: reviewNavigateStackView.reviewNavigateButtonDidTap, helpSectionDidTap: helpSectionView.getSelectedItem(type: .announcement), changePasswordSectionDidTap: accountSectionView.getSelectedItem(type: .changePassword), logoutPublisher: logoutPublisher, @@ -107,10 +108,10 @@ public final class MyPageViewController: BaseViewController { ) }).disposed(by: disposeBag) -// output.writableReviewList -// .bind(onNext: { [weak self] in -// self?.reviewNavigateStackView.setList(writableReviewCompanylist: $0) -// }).disposed(by: disposeBag) + output.writableReviewList + .bind(onNext: { [weak self] in + self?.reviewNavigateStackView.setList(writableReviewCompanylist: $0) + }).disposed(by: disposeBag) } public override func configureViewController() { diff --git a/Projects/Presentation/Sources/MyPage/MyPageViewModel.swift b/Projects/Presentation/Sources/MyPage/MyPageViewModel.swift index 780e48de..244da3f5 100644 --- a/Projects/Presentation/Sources/MyPage/MyPageViewModel.swift +++ b/Projects/Presentation/Sources/MyPage/MyPageViewModel.swift @@ -34,7 +34,7 @@ public final class MyPageViewModel: BaseViewModel, Stepper { public struct Input { let viewAppear: PublishRelay -// let reviewNavigate: PublishRelay + let reviewNavigate: PublishRelay let helpSectionDidTap: Observable let changePasswordSectionDidTap: Observable let logoutPublisher: PublishRelay @@ -56,16 +56,15 @@ public final class MyPageViewModel: BaseViewModel, Stepper { .bind(to: studentInfo) .disposed(by: disposeBag) -// input.viewAppear.asObservable() -// .flatMap { self.fetchWritableReviewListUseCase.execute() } -// .bind(to: writableReviewList) -// .disposed(by: disposeBag) + input.viewAppear.asObservable() + .flatMap { self.fetchWritableReviewListUseCase.execute() } + .bind(to: writableReviewList) + .disposed(by: disposeBag) -// input.reviewNavigate.asObservable() -// .subscribe(onNext: { -// // TODO: 리뷰 리스트로 네비게이션 이동 해주는 코드 았어야함 -// print($0) -// }).disposed(by: disposeBag) + input.reviewNavigate.asObservable() + .map { MyPageStep.writableReviewIsRequired($0) } + .bind(to: steps) + .disposed(by: disposeBag) input.helpSectionDidTap.asObservable() .map { _ in MyPageStep.noticeIsRequired } diff --git a/Projects/Presentation/Sources/RecruitmentFilter/Cell/TechTableViewCell.swift b/Projects/Presentation/Sources/RecruitmentFilter/Cell/TechTableViewCell.swift index 87582aa7..357d3f67 100644 --- a/Projects/Presentation/Sources/RecruitmentFilter/Cell/TechTableViewCell.swift +++ b/Projects/Presentation/Sources/RecruitmentFilter/Cell/TechTableViewCell.swift @@ -7,8 +7,8 @@ import RxSwift import RxCocoa final class TechStackViewCell: BaseView { - public var code: Int? - public var techCheckBoxDidTap: ((Int?) -> Void)? + public var code: CodeEntity? + public var techCheckBoxDidTap: ((CodeEntity?) -> Void)? public var isCheck: Bool = false { didSet { techCheckBoxDidTap?(code) @@ -51,7 +51,7 @@ final class TechStackViewCell: BaseView { } func adapt(model: CodeEntity) { - self.code = model.code + self.code = model self.techLabel.setJobisText(model.keyword, font: .body, color: .GrayScale.gray70) } } diff --git a/Projects/Presentation/Sources/RecruitmentFilter/Components/TechStackView.swift b/Projects/Presentation/Sources/RecruitmentFilter/Components/TechStackView.swift index 95c019f6..82d89512 100644 --- a/Projects/Presentation/Sources/RecruitmentFilter/Components/TechStackView.swift +++ b/Projects/Presentation/Sources/RecruitmentFilter/Components/TechStackView.swift @@ -7,7 +7,7 @@ import RxSwift import DesignSystem class TechStackView: UIStackView { - public var techDidTap: ((String) -> Void)? + public var techDidTap: ((CodeEntity) -> Void)? private let disposeBag = DisposeBag() init() { @@ -31,7 +31,7 @@ class TechStackView: UIStackView { techStackViewCell.adapt(model: data) techStackViewCell.techCheckBoxDidTap = { - self.techDidTap?("\($0 ?? 0)") + self.techDidTap?($0 ?? CodeEntity(code: 0, keyword: "")) } self.addArrangedSubview(techStackViewCell) } diff --git a/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewController.swift b/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewController.swift index b01d54b9..9e153285 100644 --- a/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewController.swift +++ b/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewController.swift @@ -10,7 +10,7 @@ import Domain public final class RecruitmentFilterViewController: BaseViewController { private let collectionViewDidTap = PublishRelay() private let filterApplyButtonDidTap = PublishRelay() - private var appendTechCode = PublishRelay() + private var appendTechCode = PublishRelay() private var resetTechCode = PublishRelay() private let searchTextField = JobisSearchTextField().then { diff --git a/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewModel.swift b/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewModel.swift index f289d4a7..62fe9715 100644 --- a/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewModel.swift +++ b/Projects/Presentation/Sources/RecruitmentFilter/RecruitmentFilterViewModel.swift @@ -22,7 +22,7 @@ public final class RecruitmentFilterViewModel: BaseViewModel, Stepper { let viewWillAppear: PublishRelay let selectJobsCode: Observable let filterApplyButtonDidTap: PublishRelay - let appendTechCode: PublishRelay + let appendTechCode: PublishRelay let resetTechCode: PublishRelay } @@ -61,7 +61,7 @@ public final class RecruitmentFilterViewModel: BaseViewModel, Stepper { input.filterApplyButtonDidTap.asObservable() .map { - return RecruitmentFilterStep.popToRecruitment( + RecruitmentFilterStep.popToRecruitment( jobCode: self.jobCode, techCode: self.techCode.value ) @@ -70,10 +70,10 @@ public final class RecruitmentFilterViewModel: BaseViewModel, Stepper { .disposed(by: disposeBag) input.appendTechCode.asObservable() - .filter { $0 != "" } + .filter { "\($0.code)" != "" } .bind { var value = self.techCode.value - value.append($0) + value.append("\($0.code)") self.techCode.accept(value) } .disposed(by: disposeBag) diff --git a/Projects/Presentation/Sources/WritableReview/AddReviewViewController.swift b/Projects/Presentation/Sources/WritableReview/AddReviewViewController.swift new file mode 100644 index 00000000..f728e176 --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/AddReviewViewController.swift @@ -0,0 +1,105 @@ +import UIKit +import RxSwift +import RxCocoa +import SnapKit +import Then +import Core +import Domain +import DesignSystem + +public final class AddReviewViewController: BaseBottomSheetViewController { + private let searchButtonDidTap = PublishRelay() + public var dismiss: ((String, String, CodeEntity) -> Void)? + private var appendTechCode = PublishRelay() + private let addReviewButtonDidTap = PublishRelay() + + private var viewIsHidden = false { + didSet { + if viewIsHidden { + addReviewView.isHidden = viewIsHidden + techCodeView.isHidden = !viewIsHidden + } + } + } + + private let addReviewView = AddReviewView() + private let techCodeView = TechCodeView() + + public override func addView() { + [ + addReviewView, + techCodeView + ].forEach(self.contentView.addSubview(_:)) + } + + public override func setLayout() { + addReviewView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(12) + } + techCodeView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(12) + } + } + + public override func bind() { + let input = AddReviewViewModel.Input( + viewWillAppear: viewWillAppearPublisher, + appendTechCode: appendTechCode, + addReviewButtonDidTap: addReviewButtonDidTap, + searchButtonDidTap: searchButtonDidTap + ) + + let output = viewModel.transform(input) + + output.techList + .bind { [weak self] in + self?.techCodeView.techStackView.setTech(techList: $0) + self?.techCodeView.techStackView.techDidTap = { code in + self?.appendTechCode.accept(code) + } + } + .disposed(by: disposeBag) + } + + public override func configureViewController() { + self.techCodeView.searchTextField.delegate = self + techCodeView.isHidden = true + appendTechCode.asObservable() + .bind { techCode in + if techCode.keyword != "" { + self.techCodeView.addReviewButton.isEnabled = true + } else { + self.techCodeView.addReviewButton.isEnabled = false + } + } + .disposed(by: disposeBag) + + addReviewView.nextButtonDidTap.asObservable() + .subscribe(onNext: { + self.viewIsHidden.toggle() + }) + .disposed(by: disposeBag) + + techCodeView.addReviewButton.rx.tap.asObservable() + .subscribe(onNext: { + self.viewModel.question.accept(self.addReviewView.questionTextField.text ?? "") + self.viewModel.answer.accept(self.addReviewView.answerTextView.text ?? "") + + self.dismiss?( + self.viewModel.question.value, + self.viewModel.answer.value, + self.viewModel.techCodeEntity + ) + self.addReviewButtonDidTap.accept(()) + }) + .disposed(by: disposeBag) + } +} + +extension AddReviewViewController: UITextFieldDelegate { + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + searchButtonDidTap.accept(textField.text ?? "") + self.view.endEditing(true) + return true + } +} diff --git a/Projects/Presentation/Sources/WritableReview/AddReviewViewModel.swift b/Projects/Presentation/Sources/WritableReview/AddReviewViewModel.swift new file mode 100644 index 00000000..211bab6f --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/AddReviewViewModel.swift @@ -0,0 +1,76 @@ +import UIKit +import RxSwift +import RxCocoa +import RxFlow +import Core +import Domain + +public final class AddReviewViewModel: BaseViewModel, Stepper { + public let steps = PublishRelay() + private let disposeBag = DisposeBag() + private let fetchCodeListUseCase: FetchCodeListUseCase + public let question = BehaviorRelay(value: "") + public let answer = BehaviorRelay(value: "") + public let techCode = BehaviorRelay(value: "") + public var techCodeEntity = CodeEntity(code: 0, keyword: "") + + init( + fetchCodeListUseCase: FetchCodeListUseCase + ) { + self.fetchCodeListUseCase = fetchCodeListUseCase + } + + public struct Input { + let viewWillAppear: PublishRelay + let appendTechCode: PublishRelay + let addReviewButtonDidTap: PublishRelay + let searchButtonDidTap: PublishRelay + } + + public struct Output { + let techList: BehaviorRelay<[CodeEntity]> + } + + public func transform(_ input: Input) -> Output { + let techList = BehaviorRelay<[CodeEntity]>(value: []) + + input.viewWillAppear.asObservable() + .flatMap { + self.fetchCodeListUseCase.execute(keyword: nil, type: .tech, parentCode: nil) + } + .bind(to: techList) + .disposed(by: disposeBag) + + input.appendTechCode.asObservable() + .filter { "\($0.code)" != "" } + .bind { + var value = self.techCode.value + value.append("\($0)") + self.techCodeEntity = $0 + self.techCode.accept(value) + } + .disposed(by: disposeBag) + + input.addReviewButtonDidTap.asObservable() + .map { + AddReviewStep.dismissToWritableReview + } + .bind(to: steps) + .disposed(by: disposeBag) + + input.searchButtonDidTap.asObservable() + .flatMap { + self.fetchCodeListUseCase.execute( + keyword: $0, + type: .tech, + parentCode: "" + ) + } + .bind(to: techList) + .disposed(by: disposeBag) + + return Output( + techList: techList + ) + } +} diff --git a/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/AddReviewView.swift b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/AddReviewView.swift new file mode 100644 index 00000000..158220e9 --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/AddReviewView.swift @@ -0,0 +1,136 @@ +import UIKit +import RxSwift +import RxCocoa +import SnapKit +import Then +import Core +import DesignSystem +import Domain + +class AddReviewView: BaseView { + private let disposeBag = DisposeBag() + public let nextButtonDidTap = PublishRelay() + + private let addReviewTitleLabel = UILabel().then { + $0.setJobisText( + "질문 추가하기", + font: .subBody, + color: .GrayScale.gray60 + ) + } + private let questionLabel = UILabel().then { + $0.setJobisText( + "질문", + font: .description, + color: .GrayScale.gray90 + ) + } + public let questionTextField = UITextField().then { + $0.placeholder = "example" + $0.setPlaceholderColor(.GrayScale.gray60) + $0.layer.cornerRadius = 12 + $0.backgroundColor = .GrayScale.gray10 + $0.font = UIFont.jobisFont(.body) + $0.addLeftPadding(size: 16) + $0.addRightPadding(size: 16) + } + private let answerLabel = UILabel().then { + $0.setJobisText( + "답변", + font: .description, + color: .GrayScale.gray90 + ) + } + public let answerTextView = UITextView().then { + $0.text = "example" + $0.layer.cornerRadius = 12 + $0.textColor = UIColor.GrayScale.gray60 + $0.backgroundColor = .GrayScale.gray10 + $0.font = UIFont.jobisFont(.body) + $0.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) + } + private let nextButton = JobisButton(style: .main).then { + $0.setText("다음") + $0.isEnabled = false + } + + public override func addView() { + [ + addReviewTitleLabel, + questionLabel, + questionTextField, + answerLabel, + answerTextView, + nextButton + ].forEach(self.addSubview(_:)) + } + + public override func setLayout() { + addReviewTitleLabel.snp.makeConstraints { + $0.top.leading.equalToSuperview().inset(24) + $0.height.equalTo(20) + } + + questionLabel.snp.makeConstraints { + $0.top.equalTo(addReviewTitleLabel.snp.bottom).offset(28) + $0.leading.equalToSuperview().inset(24) + } + + questionTextField.snp.makeConstraints { + $0.top.equalTo(questionLabel.snp.bottom).offset(4) + $0.leading.trailing.equalToSuperview().inset(24) + $0.height.equalTo(48) + } + + answerLabel.snp.makeConstraints { + $0.top.equalTo(questionTextField.snp.bottom).offset(24) + $0.leading.equalToSuperview().inset(24) + } + + answerTextView.snp.makeConstraints { + $0.top.equalTo(answerLabel.snp.bottom).offset(4) + $0.leading.trailing.equalToSuperview().inset(24) + $0.bottom.equalTo(nextButton.snp.top).inset(-24) + } + + nextButton.snp.makeConstraints { + $0.bottom.equalToSuperview().inset(12) + $0.leading.trailing.equalToSuperview().inset(24) + $0.height.equalTo(56) + } + } + + public override func configureView() { + answerTextView.delegate = self + let textFieldIsEmpty = questionTextField.rx.text.orEmpty.map { !$0.isEmpty } + let textViewIsEmpty = answerTextView.rx.text.orEmpty.map { + !$0.isEmpty && $0 != "example" + } + + Observable.combineLatest(textFieldIsEmpty, textViewIsEmpty) { $0 && $1 } + .bind(to: nextButton.rx.isEnabled) + .disposed(by: disposeBag) + + nextButton.rx.tap.asObservable() + .subscribe(onNext: { + self.nextButtonDidTap.accept(()) + }) + .disposed(by: disposeBag) + } +} + +extension AddReviewView: UITextViewDelegate { + public func textViewDidBeginEditing(_ textView: UITextView) { + if textView.textColor == UIColor.GrayScale.gray60 { + textView.text = nil + textView.textColor = UIColor.GrayScale.gray90 + } + } + + public func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.isEmpty { + textView.text = "example" + textView.textColor = UIColor.GrayScale.gray60 + } + } +} diff --git a/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/Cell/TechCodeTableViewCell.swift b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/Cell/TechCodeTableViewCell.swift new file mode 100644 index 00000000..59bc6cd8 --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/Cell/TechCodeTableViewCell.swift @@ -0,0 +1,60 @@ +import UIKit +import Domain +import DesignSystem +import SnapKit +import Then +import RxSwift +import RxCocoa + +final class TechCodeStackViewCell: BaseView { + public var code: CodeEntity? + public var techCheckBoxDidTap: ((CodeEntity?, Bool) -> Void)? + public var isCheck: Bool = false { + didSet { + techCheckBoxDidTap?(code, isCheck) + techCheckBox.isCheck = isCheck + } + } + + private let backStackView = UIStackView().then { + $0.spacing = 8 + $0.axis = .horizontal + $0.isLayoutMarginsRelativeArrangement = true + $0.layoutMargins = .init(top: 12, left: 24, bottom: 12, right: 24) + } + private let techCheckBox = JobisCheckBox() + private let techLabel = UILabel().then { + $0.text = "-" + } + private var disposeBag = DisposeBag() + + override func addView() { + self.addSubview(backStackView) + [ + techCheckBox, + techLabel + ].forEach(self.backStackView.addArrangedSubview(_:)) + } + + override func setLayout() { + techCheckBox.snp.makeConstraints { + $0.width.height.equalTo(28) + } + backStackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + override func configureView() { + techCheckBox.rx.tap.asObservable() + .bind { [weak self] in + self?.isCheck.toggle() + } + .disposed(by: disposeBag) + } + + func adapt(model: CodeEntity) { + self.code = model + self.techLabel.setJobisText(model.keyword, font: .body, color: .GrayScale.gray70) + } +} diff --git a/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/TechCodeStackView.swift b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/TechCodeStackView.swift new file mode 100644 index 00000000..9865c4ed --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/TechCodeStackView.swift @@ -0,0 +1,61 @@ +import UIKit +import Domain +import SnapKit +import Then +import RxGesture +import RxSwift +import RxCocoa +import DesignSystem + +public class TechCodeStackView: UIStackView { + private let techCodeView = TechCodeView() + public var techDidTap: ((CodeEntity) -> Void)? + private let disposeBag = DisposeBag() + private var selectedCell: TechCodeStackViewCell? + + init() { + super.init(frame: .zero) + self.axis = .vertical + self.spacing = 0 + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setTech(techList: [CodeEntity]) { + self.subviews.forEach { + self.removeArrangedSubview($0) + $0.removeFromSuperview() + } + + techList.enumerated().forEach { index, data in + let techCodeStackViewCell = TechCodeStackViewCell() + techCodeStackViewCell.adapt(model: data) + + techCodeStackViewCell.techCheckBoxDidTap = { [weak self] code, isCheck in + guard let self = self else { return } + + if let selectedCell = self.selectedCell, selectedCell != techCodeStackViewCell { + selectedCell.isCheck = false + } + + self.selectedCell = techCodeStackViewCell + if isCheck { + self.techDidTap?(code ?? CodeEntity(code: 0, keyword: "")) + } else { + self.techDidTap?(CodeEntity(code: 0, keyword: "")) + } + self.techCodeView.area.accept(data.keyword) + } + self.addArrangedSubview(techCodeStackViewCell) + } + } + + func cellAtIndex(_ index: Int) -> TechCodeStackViewCell? { + guard index >= 0 && index < self.arrangedSubviews.count else { + return nil + } + return self.arrangedSubviews[index] as? TechCodeStackViewCell + } +} diff --git a/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/TechCodeView.swift b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/TechCodeView.swift new file mode 100644 index 00000000..734deb9b --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/BottomSheet/Component/TechCodeView.swift @@ -0,0 +1,94 @@ +import UIKit +import RxSwift +import RxCocoa +import SnapKit +import Then +import Core +import DesignSystem +import Domain + +public final class TechCodeView: BaseView { + private let disposeBag = DisposeBag() + public var area = BehaviorRelay(value: "") + + private let techCodeTitleLabel = UILabel().then { + $0.setJobisText( + "기술 스택", + font: .subBody, + color: .GrayScale.gray60 + ) + } + private let searchImageView = UIImageView().then { + $0.image = .jobisIcon(.searchIcon) + } + public let searchTextField = UITextField().then { + $0.placeholder = "검색어를 입력해주세요" + $0.setPlaceholderColor(.GrayScale.gray60) + $0.layer.cornerRadius = 12 + $0.backgroundColor = .GrayScale.gray10 + $0.font = UIFont.jobisFont(.body) + $0.addLeftPadding(size: 44) + $0.addRightPadding(size: 16) + } + private let scrollView = UIScrollView() + private let contentStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 12 + } + public lazy var techStackView = TechCodeStackView() + public let addReviewButton = JobisButton(style: .main).then { + $0.setText("다음") + $0.isEnabled = false + } + + public override func addView() { + [ + techCodeTitleLabel, + searchTextField, + scrollView, + addReviewButton + ].forEach(self.addSubview(_:)) + searchTextField.addSubview(searchImageView) + scrollView.addSubview(contentStackView) + [ + techStackView + ].forEach(self.contentStackView.addArrangedSubview(_:)) + } + + public override func setLayout() { + techCodeTitleLabel.snp.makeConstraints { + $0.top.leading.equalToSuperview().inset(24) + $0.height.equalTo(20) + } + + searchImageView.snp.makeConstraints { + $0.centerY.equalTo(searchTextField) + $0.left.equalToSuperview().inset(16) + } + + searchTextField.snp.makeConstraints { + $0.top.equalTo(techCodeTitleLabel.snp.bottom).offset(24) + $0.leading.trailing.equalToSuperview().inset(24) + $0.height.equalTo(48) + } + + scrollView.snp.makeConstraints { + $0.top.equalTo(searchTextField.snp.bottom).offset(8) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(addReviewButton.snp.top).inset(-12) + } + + contentStackView.snp.makeConstraints { + $0.edges.equalTo(scrollView.contentLayoutGuide) + $0.width.equalToSuperview() + } + + addReviewButton.snp.makeConstraints { + $0.height.equalTo(56) + $0.leading.trailing.equalToSuperview().inset(24) + $0.bottom.equalToSuperview().inset(12) + } + } + + override public func configureView() {} +} diff --git a/Projects/Presentation/Sources/WritableReview/Components/QuestionListDetailStackView.swift b/Projects/Presentation/Sources/WritableReview/Components/QuestionListDetailStackView.swift new file mode 100644 index 00000000..de27abd9 --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/Components/QuestionListDetailStackView.swift @@ -0,0 +1,36 @@ +import UIKit +import Domain +import SnapKit +import Then +import DesignSystem + +public final class QuestionListDetailStackView: BaseView { + private let backStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 8 + $0.layoutMargins = .init(top: 4, left: 0, bottom: 4, right: 0) + $0.isLayoutMarginsRelativeArrangement = true + } + public override func addView() { + [ + backStackView + ].forEach(self.addSubview(_:)) + } + + public override func setLayout() { + backStackView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.leading.trailing.equalToSuperview() + $0.bottom.equalToSuperview() + } + } + + func setFieldType(_ list: [QnaEntity]) { + list.forEach { data in + let attachmentView = QuestionListDetailView().then { + $0.configureView(model: data) + } + self.backStackView.addArrangedSubview(attachmentView) + } + } +} diff --git a/Projects/Presentation/Sources/WritableReview/Components/QuestionListDetailView.swift b/Projects/Presentation/Sources/WritableReview/Components/QuestionListDetailView.swift new file mode 100644 index 00000000..93d627d4 --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/Components/QuestionListDetailView.swift @@ -0,0 +1,159 @@ +import UIKit +import SnapKit +import Then +import Domain +import DesignSystem +import RxSwift + +final class QuestionListDetailView: BaseView { + public var isOpen = false { + didSet { + UIView.animate( + withDuration: 0.35, + delay: 0, + usingSpringWithDamping: 0.9, + initialSpringVelocity: 1, + options: .transitionCrossDissolve + ) { [self] in + detailView.arrangedSubviews.forEach { $0.isHidden = !isOpen } + detailView.arrangedSubviews.forEach { $0.alpha = isOpen ? 1 : 0 } + interviewReviewArrowImageView.image = .jobisIcon(isOpen ? .arrowUp : .arrowDown) + self.layoutIfNeeded() + } + } + } + private let disposeBag = DisposeBag() + private let backStackView = UIView().then { + $0.backgroundColor = .GrayScale.gray30 + $0.layer.cornerRadius = 12 + $0.clipsToBounds = true + } + private let titleHeadView = UIStackView().then { + $0.axis = .horizontal + } + private let titleView = UIStackView().then { + $0.axis = .horizontal + $0.alignment = .leading + $0.spacing = 8 + } + private let titleSubView = UIStackView().then { + $0.axis = .vertical + $0.alignment = .leading + } + private let qSymbolLabel = UILabel().then { + $0.setJobisText( + "Q", + font: .subHeadLine, + color: .Primary.blue20 + ) + } + private let questionLabel = UILabel().then { + $0.setJobisText( + "-", + font: .subHeadLine, + color: .GrayScale.gray90 + ) + $0.numberOfLines = 0 + } + private let codeLabel = UILabel().then { + $0.setJobisText( + "-", + font: .description, + color: .Sub.skyBlue20 + ) + } + private let interviewReviewArrowImageView = UIImageView().then { + $0.image = .jobisIcon(.arrowDown) + } + private let detailView = UIStackView().then { + $0.spacing = 0 + $0.axis = .horizontal + $0.isLayoutMarginsRelativeArrangement = true + $0.layoutMargins = .init(top: 16, left: 0, bottom: 0, right: 0) + } + private let aSymbolLabel = UILabel().then { + $0.setJobisText( + "A", + font: .subHeadLine, + color: .Primary.blue20 + ) + $0.isHidden = true + } + private let answerLabel = UILabel().then { + $0.setJobisText( + "-", + font: .description, + color: .GrayScale.gray70 + ) + $0.numberOfLines = 0 + $0.isHidden = true + } + + override func addView() { + self.addSubview(backStackView) + + [ + titleHeadView, + detailView + ].forEach(self.backStackView.addSubview(_:)) + + [ + questionLabel, + codeLabel + ].forEach(self.titleSubView.addArrangedSubview(_:)) + + [ + qSymbolLabel, + titleSubView + ].forEach(self.titleView.addArrangedSubview(_:)) + + [ + titleView, + interviewReviewArrowImageView + ].forEach(self.titleHeadView.addArrangedSubview(_:)) + + [ + aSymbolLabel, + answerLabel + ].forEach(self.detailView.addArrangedSubview(_:)) + } + + override func setLayout() { + titleHeadView.snp.makeConstraints { + $0.top.equalToSuperview().inset(12) + $0.leading.trailing.equalToSuperview().inset(16) + } + + detailView.snp.updateConstraints { + $0.top.equalTo(titleHeadView.snp.bottom) + $0.leading.trailing.equalToSuperview().inset(16) + $0.bottom.equalToSuperview().inset(isOpen ? 16 : 0) + } + + interviewReviewArrowImageView.snp.makeConstraints { + $0.width.equalTo(24) + } + + [ + aSymbolLabel, + answerLabel + ].forEach { detailView.setCustomSpacing(12, after: $0)} + + backStackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + func configureView(model: QnaEntity) { + super.configureView() + questionLabel.text = model.question + codeLabel.text = model.area + answerLabel.text = model.answer + self.rx.tapGesture() + .when(.recognized) + .bind { _ in + self.isOpen.toggle() + } + .disposed(by: disposeBag) + } +} diff --git a/Projects/Presentation/Sources/WritableReview/WritableReviewViewController.swift b/Projects/Presentation/Sources/WritableReview/WritableReviewViewController.swift new file mode 100644 index 00000000..02f69746 --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/WritableReviewViewController.swift @@ -0,0 +1,157 @@ +import UIKit +import Domain +import RxSwift +import RxCocoa +import SnapKit +import Then +import Core +import DesignSystem + +public final class WritableReviewViewController: BaseViewController { + private let addQuestionButtonDidTap = PublishRelay() + private let writableReviewButtonDidTap = PublishRelay() + + private let scrollView = UIScrollView().then { + $0.showsVerticalScrollIndicator = false + } + private let contentView = UIView() + private let pageTitleLabel = UILabel().then { + $0.setJobisText( + "다른 학생들을 위하여\n면접의 후기를 작성해주세요", + font: .pageTitle, + color: .GrayScale.gray90 + ) + $0.numberOfLines = 2 + } + private let mainStackView = UIStackView().then { + $0.axis = .vertical + $0.spacing = 4 + $0.layoutMargins = .init(top: 0, left: 24, bottom: 0, right: 24) + $0.isLayoutMarginsRelativeArrangement = true + } + private let emptyQuestionListView = UIView().then { + $0.layer.cornerRadius = 12 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.GrayScale.gray40.cgColor + } + private let emptyQuestionLabel = UILabel().then { + $0.setJobisText( + "현재 입력 된 질문이 없어요", + font: .subBody, + color: .GrayScale.gray60 + ) + } + private let questionListDetailStackView = QuestionListDetailStackView() + private let addQuestionButton = JobisButton(style: .sub).then { + $0.setText("질문 추가하기") + } + private var writableReviewButton = JobisButton(style: .main).then { + $0.setText("후기를 작성해주세요") + $0.isEnabled = false + } + + public override func addView() { + emptyQuestionListView.addSubview(emptyQuestionLabel) + + [ + emptyQuestionListView, + questionListDetailStackView, + addQuestionButton + ].forEach { mainStackView.addArrangedSubview($0) } + + [ + pageTitleLabel, + scrollView, + writableReviewButton + ].forEach { view.addSubview($0) } + scrollView.addSubview(contentView) + contentView.addSubview(mainStackView) + } + + public override func setLayout() { + emptyQuestionListView.snp.makeConstraints { + $0.height.equalTo(52) + } + + emptyQuestionLabel.snp.makeConstraints { + $0.centerX.centerY.equalToSuperview() + } + + pageTitleLabel.snp.makeConstraints { + $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).inset(20) + $0.leading.trailing.equalToSuperview().inset(24) + } + + scrollView.snp.makeConstraints { + $0.top.equalTo(pageTitleLabel.snp.bottom).offset(20) + $0.leading.trailing.equalTo(self.view.safeAreaLayoutGuide) + $0.bottom.equalTo(writableReviewButton.snp.top).inset(-12) + } + + contentView.snp.makeConstraints { + $0.edges.equalTo(scrollView.contentLayoutGuide) + $0.width.equalToSuperview() + $0.bottom.equalTo(mainStackView.snp.bottom).offset(20) + } + + mainStackView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + } + + writableReviewButton.snp.makeConstraints { + $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(12) + $0.leading.trailing.equalToSuperview().inset(24) + } + } + + public override func bind() { + let input = WritableReviewViewModel.Input( + viewWillAppear: self.viewWillAppearPublisher, + addQuestionButtonDidTap: addQuestionButtonDidTap, + writableReviewButtonDidTap: writableReviewButtonDidTap + ) + + let output = viewModel.transform(input) + + output.qnaInfoList.asObservable() + .bind(onNext: { + self.questionListDetailStackView.setFieldType($0) + }) + .disposed(by: disposeBag) + + output.interviewReviewInfoList.asObservable() + .bind(onNext: { + self.emptyQuestionListView.isHidden = !$0.isEmpty + if !$0.isEmpty { + self.showJobisToast(text: "질문이 추가되었어요!", inset: 92) + self.writableReviewButton.isEnabled = true + self.writableReviewButton.setText("작성 완료") + } + }) + .disposed(by: disposeBag) + } + + public override func configureViewController() { + self.viewWillAppearPublisher.asObservable() + .subscribe(onNext: { + self.hideTabbar() + }) + .disposed(by: disposeBag) + + addQuestionButton.rx.tap.asObservable() + .subscribe(onNext: { + self.addQuestionButtonDidTap.accept(()) + }) + .disposed(by: disposeBag) + + writableReviewButton.rx.tap.asObservable() + .subscribe(onNext: { + self.writableReviewButtonDidTap.accept(()) + }) + .disposed(by: disposeBag) + } + + public override func configureNavigation() { + self.navigationController?.navigationBar.prefersLargeTitles = false + } +} diff --git a/Projects/Presentation/Sources/WritableReview/WritableReviewViewModel.swift b/Projects/Presentation/Sources/WritableReview/WritableReviewViewModel.swift new file mode 100644 index 00000000..a1197f30 --- /dev/null +++ b/Projects/Presentation/Sources/WritableReview/WritableReviewViewModel.swift @@ -0,0 +1,78 @@ +import UIKit +import RxSwift +import RxCocoa +import RxFlow +import Core +import Domain + +public final class WritableReviewViewModel: BaseViewModel, Stepper { + public let steps = PublishRelay() + private let disposeBag = DisposeBag() + public var companyID = 0 + private let postReviewUseCase: PostReviewUseCase + public var interviewReviewInfo = PublishRelay() + public var qnaInfoList = PublishRelay<[QnaEntity]>() + public var interviewReviewInfoList = BehaviorRelay<[QnaElementRequestQuery]>(value: []) + public var techCode: Int? + + init( + postReviewUseCase: PostReviewUseCase + ) { + self.postReviewUseCase = postReviewUseCase + } + + public struct Input { + let viewWillAppear: PublishRelay + let addQuestionButtonDidTap: PublishRelay + let writableReviewButtonDidTap: PublishRelay + } + + public struct Output { + let interviewReviewInfoList: BehaviorRelay<[QnaElementRequestQuery]> + let qnaInfoList: PublishRelay<[QnaEntity]> + } + + public func transform(_ input: Input) -> Output { + input.addQuestionButtonDidTap.asObservable() + .map { + WritableReviewStep.addReviewIsRequired + } + .bind(to: steps) + .disposed(by: disposeBag) + + self.interviewReviewInfo.asObservable() + .subscribe(onNext: { qnaEntity in + self.qnaInfoList.accept([qnaEntity]) + var value = self.interviewReviewInfoList.value + value.append( + QnaElementRequestQuery( + question: qnaEntity.question, + answer: qnaEntity.answer, + codeID: self.techCode ?? 0 + ) + ) + self.interviewReviewInfoList.accept(value) + }) + .disposed(by: disposeBag) + + input.writableReviewButtonDidTap.asObservable() + .flatMap { + self.postReviewUseCase.execute(req: PostReviewRequestQuery( + companyID: self.companyID, + qnaElements: self.interviewReviewInfoList.value + )) + } + .subscribe() + .disposed(by: disposeBag) + + input.writableReviewButtonDidTap.asObservable() + .map { _ in WritableReviewStep.popToMyPage } + .bind(to: steps) + .disposed(by: disposeBag) + + return Output( + interviewReviewInfoList: interviewReviewInfoList, + qnaInfoList: self.qnaInfoList + ) + } +}