diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift index 32bcf904..481ed5af 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift @@ -19,7 +19,7 @@ struct PublicCourse: Codable { let id, courseId: Int let title: String let image: String - let scarp: Bool? + let scrap: Bool? let description: String? let distance: Float? let departure: CourseDiscoveryDeparture diff --git a/Runnect-iOS/Runnect-iOS/Network/Router/CourseDetailRouter/UploadedCourseDetailRouter.swift b/Runnect-iOS/Runnect-iOS/Network/Router/CourseDetailRouter/UploadedCourseDetailRouter.swift index caf87e03..2e0446e5 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Router/CourseDetailRouter/UploadedCourseDetailRouter.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Router/CourseDetailRouter/UploadedCourseDetailRouter.swift @@ -11,6 +11,7 @@ import Moya enum UploadedCourseDetailRouter { case getUploadedCourseDetail(publicCourseId: Int) + case createAndDeleteScrap(publicCourseId: Int, scrapTF: Bool) } extension UploadedCourseDetailRouter: TargetType { @@ -26,6 +27,8 @@ extension UploadedCourseDetailRouter: TargetType { switch self { case .getUploadedCourseDetail(let publicCourseId): return "/public-course/detail/\(publicCourseId)" + case .createAndDeleteScrap: + return "/scrap" } } @@ -33,6 +36,8 @@ extension UploadedCourseDetailRouter: TargetType { switch self { case .getUploadedCourseDetail: return .get + case .createAndDeleteScrap: + return .post } } @@ -40,12 +45,14 @@ extension UploadedCourseDetailRouter: TargetType { switch self { case .getUploadedCourseDetail: return .requestPlain + case .createAndDeleteScrap(let publicCourseId, let scrapTF): + return .requestParameters(parameters: ["publicCourseId": publicCourseId, "scrapTF": scrapTF], encoding: JSONEncoding.default) } } var headers: [String: String]? { switch self { - case .getUploadedCourseDetail: + case .getUploadedCourseDetail, .createAndDeleteScrap: return Config.headerWithDeviceId } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift index c3b046f5..a3413c90 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift @@ -115,7 +115,7 @@ final class CourseDetailVC: UIViewController { extension CourseDetailVC { @objc func likeButtonDidTap(_ sender: UIButton) { - sender.isSelected.toggle() + scrapCourse(scrapTF: !sender.isSelected) } @objc func startButtonDidTap() { @@ -160,6 +160,9 @@ extension CourseDetailVC { self.runningLevelLabel.text = "Lv. \(model.user.level)" self.courseTitleLabel.text = model.publicCourse.title + guard let scrap = model.publicCourse.scrap else { return } + self.likeButton.isSelected = scrap + guard let distance = model.publicCourse.distance else { return } self.courseDistanceInfoView.setDescriptionText(description: String(distance)) let location = "\(model.publicCourse.departure.region) \(model.publicCourse.departure.city)" @@ -320,7 +323,6 @@ extension CourseDetailVC { private func getCourseDetailWithPath(courseId: Int) { LoadingIndicator.showLoading() - runningProvider.request(.getCourseDetail(courseId: courseId)) { [weak self] response in guard let self = self else { return } LoadingIndicator.hideLoading() @@ -347,4 +349,27 @@ extension CourseDetailVC { } } } + + private func scrapCourse(scrapTF: Bool) { + guard let publicCourseId = self.publicCourseId else { return } + LoadingIndicator.showLoading() + courseDetailProvider.request(.createAndDeleteScrap(publicCourseId: publicCourseId, scrapTF: scrapTF)) { [weak self] response in + LoadingIndicator.hideLoading() + guard let self = self else { return } + switch response { + case .success(let result): + let status = result.statusCode + if 200..<300 ~= status { + self.likeButton.isSelected.toggle() + } + if status >= 400 { + print("400 error") + self.showNetworkFailureToast() + } + case .failure(let error): + print(error.localizedDescription) + self.showNetworkFailureToast() + } + } + } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift index 75115340..5d989eb5 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift @@ -15,7 +15,11 @@ import Moya final class CourseDiscoveryVC: UIViewController { // MARK: - Properties - let pickedMapListProvider = MoyaProvider( + private let pickedMapListProvider = MoyaProvider( + plugins: [NetworkLoggerPlugin(verbose: true)] + ) + + private let courseDetailProvider = MoyaProvider( plugins: [NetworkLoggerPlugin(verbose: true)] ) @@ -167,9 +171,10 @@ extension CourseDiscoveryVC: UICollectionViewDelegate, UICollectionViewDataSourc } else { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CourseListCVC.className, for: indexPath) as? CourseListCVC else { return UICollectionViewCell() } cell.setCellType(type: .all) + cell.delegate = self let model = self.courseList[indexPath.item] let location = "\(model.departure.region) \(model.departure.city)" - cell.setData(imageURL: model.image, title: model.title, location: location, didLike: model.scarp) + cell.setData(imageURL: model.image, title: model.title, location: location, didLike: model.scrap, indexPath: indexPath.item) return cell } } @@ -230,6 +235,15 @@ extension CourseDiscoveryVC: UICollectionViewDelegateFlowLayout { } } +// MARK: - CourseListCVCDeleagte + +extension CourseDiscoveryVC: CourseListCVCDeleagte { + func likeButtonTapped(wantsTolike: Bool, index: Int) { + let publicCourseId = courseList[index].id + scrapCourse(publicCourseId: publicCourseId, scrapTF: wantsTolike) + } +} + // MARK: - Network extension CourseDiscoveryVC { @@ -259,4 +273,26 @@ extension CourseDiscoveryVC { } } } + + private func scrapCourse(publicCourseId: Int, scrapTF: Bool) { + LoadingIndicator.showLoading() + courseDetailProvider.request(.createAndDeleteScrap(publicCourseId: publicCourseId, scrapTF: scrapTF)) { [weak self] response in + LoadingIndicator.hideLoading() + guard let self = self else { return } + switch response { + case .success(let result): + let status = result.statusCode + if 200..<300 ~= status { + self.getCourseData() + } + if status >= 400 { + print("400 error") + self.showNetworkFailureToast() + } + case .failure(let error): + print(error.localizedDescription) + self.showNetworkFailureToast() + } + } + } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift index 80fc15bc..60c43279 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift @@ -19,7 +19,12 @@ final class CourseSearchVC: UIViewController { plugins: [NetworkLoggerPlugin(verbose: true)] ) + private let courseDetailProvider = MoyaProvider( + plugins: [NetworkLoggerPlugin(verbose: true)] + ) + private var courseList = [PublicCourse]() + private var keyword: String? // MARK: - UI Components @@ -71,7 +76,12 @@ final class CourseSearchVC: UIViewController { setDelegate() layout() setTabBar() - + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + guard let keyword = self.keyword else { return } + searchCourseWithKeyword(keyword: keyword) } } // MARK: - Methods @@ -138,9 +148,10 @@ extension CourseSearchVC: UICollectionViewDelegate, UICollectionViewDataSource { for: indexPath) as? CourseListCVC else { return UICollectionViewCell() } cell.setCellType(type: .all) + cell.delegate = self let model = self.courseList[indexPath.item] let location = "\(model.departure.region) \(model.departure.city)" - cell.setData(imageURL: model.image, title: model.title, location: location, didLike: model.scarp) + cell.setData(imageURL: model.image, title: model.title, location: location, didLike: model.scrap, indexPath: indexPath.item) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { @@ -184,8 +195,19 @@ extension CourseSearchVC: UICollectionViewDelegateFlowLayout { extension CourseSearchVC: CustomNavigationBarDelegate { func searchButtonDidTap(text: String) { searchCourseWithKeyword(keyword: text) + self.keyword = text + } +} + +// MARK: - CourseListCVCDeleagte + +extension CourseSearchVC: CourseListCVCDeleagte { + func likeButtonTapped(wantsTolike: Bool, index: Int) { + let pubilcCourseId = courseList[index].id + scrapCourse(publicCourseId: pubilcCourseId, scrapTF: wantsTolike) } } + // MARK: - Network extension CourseSearchVC { @@ -215,4 +237,23 @@ extension CourseSearchVC { } } } + + private func scrapCourse(publicCourseId: Int, scrapTF: Bool) { + LoadingIndicator.showLoading() + courseDetailProvider.request(.createAndDeleteScrap(publicCourseId: publicCourseId, scrapTF: scrapTF)) { [weak self] response in + LoadingIndicator.hideLoading() + guard let self = self else { return } + switch response { + case .success(let result): + let status = result.statusCode + if status >= 400 { + print("400 error") + self.showNetworkFailureToast() + } + case .failure(let error): + print(error.localizedDescription) + self.showNetworkFailureToast() + } + } + } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift index 4f1c1904..f2f0b30d 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift @@ -14,6 +14,10 @@ final class CourseStorageVC: UIViewController { // MARK: - Properties + private let courseDetailProvider = MoyaProvider( + plugins: [NetworkLoggerPlugin(verbose: true)] + ) + private let courseStorageProvider = MoyaProvider( plugins: [NetworkLoggerPlugin(verbose: true)] ) @@ -42,6 +46,7 @@ final class CourseStorageVC: UIViewController { self.setUI() self.setLayout() self.bindUI() + self.setDelegate() } override func viewWillAppear(_ animated: Bool) { @@ -92,6 +97,10 @@ extension CourseStorageVC { self.navigationController?.pushViewController(courseDetailVC, animated: true) }.store(in: cancelBag) } + + private func setDelegate() { + scrapCourseListView.delegate = self + } } // MARK: - UI & Layout @@ -116,6 +125,14 @@ extension CourseStorageVC { } } +// MARK: - ScrapCourseListViewDelegate + +extension CourseStorageVC: ScrapCourseListViewDelegate { + func likeButtonTapped(wantsTolike: Bool, publicCourseId: Int) { + scrapCourse(publicCourseId: publicCourseId, scrapTF: wantsTolike) + } +} + // MARK: - Network extension CourseStorageVC { @@ -174,4 +191,26 @@ extension CourseStorageVC { } } } + + private func scrapCourse(publicCourseId: Int, scrapTF: Bool) { + LoadingIndicator.showLoading() + courseDetailProvider.request(.createAndDeleteScrap(publicCourseId: publicCourseId, scrapTF: scrapTF)) { [weak self] response in + LoadingIndicator.hideLoading() + guard let self = self else { return } + switch response { + case .success(let result): + let status = result.statusCode + if 200..<300 ~= status { + self.getScrapCourseList() + } + if status >= 400 { + print("400 error") + self.showNetworkFailureToast() + } + case .failure(let error): + print(error.localizedDescription) + self.showNetworkFailureToast() + } + } + } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/CourseListCVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/CourseListCVC.swift index e1248df7..9491930d 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/CourseListCVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/CourseListCVC.swift @@ -8,7 +8,7 @@ import UIKit protocol CourseListCVCDeleagte: AnyObject { - func likeButtonTapped(wantsTolike: Bool) + func likeButtonTapped(wantsTolike: Bool, index: Int) } @frozen @@ -34,6 +34,8 @@ final class CourseListCVC: UICollectionViewCell { weak var delegate: CourseListCVCDeleagte? + private var indexPath: Int? + // MARK: - UI Components private let courseImageView = UIImageView().then { @@ -65,7 +67,6 @@ final class CourseListCVC: UICollectionViewCell { private let likeButton = UIButton(type: .custom).then { $0.setImage(ImageLiterals.icHeartFill, for: .selected) $0.setImage(ImageLiterals.icHeart, for: .normal) - $0.isSelected = true $0.backgroundColor = .w1 } @@ -90,9 +91,10 @@ extension CourseListCVC { likeButton.addTarget(self, action: #selector(likeButtonDidTap), for: .touchUpInside) } - func setData(imageURL: String, title: String, location: String?, didLike: Bool?) { + func setData(imageURL: String, title: String, location: String?, didLike: Bool?, indexPath: Int? = nil) { self.courseImageView.setImage(with: imageURL) self.titleLabel.text = title + self.indexPath = indexPath if let location = location { self.locationLabel.text = location @@ -117,8 +119,9 @@ extension CourseListCVC { extension CourseListCVC { @objc func likeButtonDidTap(_ sender: UIButton) { + guard let indexPath = self.indexPath else { return } sender.isSelected.toggle() - delegate?.likeButtonTapped(wantsTolike: (sender.isSelected == true)) + delegate?.likeButtonTapped(wantsTolike: (sender.isSelected == true), index: indexPath) } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/ScrapCourseListView.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/ScrapCourseListView.swift index 4c1bb029..5424cbf5 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/ScrapCourseListView.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/ScrapCourseListView.swift @@ -8,6 +8,10 @@ import UIKit import Combine +protocol ScrapCourseListViewDelegate: AnyObject { + func likeButtonTapped(wantsTolike: Bool, publicCourseId: Int) +} + final class ScrapCourseListView: UIView { // MARK: - Properties @@ -16,6 +20,7 @@ final class ScrapCourseListView: UIView { var cellDidTapped = PassthroughSubject() private var courseList = [ScrapCourse]() + weak var delegate: ScrapCourseListViewDelegate? final let collectionViewInset = UIEdgeInsets(top: 28, left: 16, bottom: 28, right: 16) final let itemSpacing: CGFloat = 10 @@ -110,12 +115,12 @@ extension ScrapCourseListView: UICollectionViewDelegate, UICollectionViewDataSou for: indexPath) as? CourseListCVC else { return UICollectionViewCell() } cell.setCellType(type: .all) - + cell.delegate = self let model = courseList[indexPath.item] let location = "\(model.departure.region) \(model.departure.city)" - cell.setData(imageURL: model.image, title: model.title, location: location, didLike: true) + cell.setData(imageURL: model.image, title: model.title, location: location, didLike: true, indexPath: indexPath.item) return cell } } @@ -154,3 +159,10 @@ extension ScrapCourseListView: ListEmptyViewDelegate { self.scrapButtonTapped.send() } } + +extension ScrapCourseListView: CourseListCVCDeleagte { + func likeButtonTapped(wantsTolike: Bool, index: Int) { + let publicCourseId = courseList[index].publicCourseId + delegate?.likeButtonTapped(wantsTolike: wantsTolike, publicCourseId: publicCourseId) + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/MyPage/VC/MyPageVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/MyPage/VC/MyPageVC.swift index fbed9a97..5815ef24 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/MyPage/VC/MyPageVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/MyPage/VC/MyPageVC.swift @@ -90,6 +90,11 @@ final class MyPageVC: UIViewController { setLayout() getMyPageInfo() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.getMyPageInfo() + } } // MARK: - Methods