From 4861844d15e1563a85f9c3db6c0ec7c8627ea312 Mon Sep 17 00:00:00 2001 From: onesunny2 Date: Thu, 7 May 2026 23:07:58 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[Feat]=20#703=20-=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?= =?UTF-8?q?=EB=B0=8F=20URL=20=EC=83=81=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resource/Constants/Strings/StringLiterals+Search.swift | 3 +++ WSSiOS/Resource/Constants/URLs/URLs.swift | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift b/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift index 852bfb100..067a01402 100644 --- a/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift +++ b/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift @@ -11,6 +11,9 @@ extension StringLiterals { enum Search { static let title = "탐색하기" static let searchbar = "작품 제목, 작가를 검색하세요" + + static let recentSearchTitle = "최근 검색어" + static let deleteAll = "전체삭제" static let induceTitle = "뭐 읽을지 고민될 땐?" static let induceDescription = "장르, 연재상태, 별점, 키워드로 작품 찾기" diff --git a/WSSiOS/Resource/Constants/URLs/URLs.swift b/WSSiOS/Resource/Constants/URLs/URLs.swift index 96e81de3a..0bd70ef9a 100644 --- a/WSSiOS/Resource/Constants/URLs/URLs.swift +++ b/WSSiOS/Resource/Constants/URLs/URLs.swift @@ -191,6 +191,11 @@ enum URLs { static let sosoPick = "/soso-picks" static let normalSearch = "/novels" static let detailSearch = "/novels/filtered" + static let recentSearch = "/novels/recent-searches" + static func deleteRecentSearchKeyword(id: Int) -> String { + return "/novels/recent-searches/\(id)" + } + static let deleteAllRecentSearchKeywords = "/novels/recent-searches" } enum Keyword { From 157db4c5b5d9a1626d0546c791226660b4a7dbc2 Mon Sep 17 00:00:00 2001 From: onesunny2 Date: Thu, 7 May 2026 23:08:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[Feat]=20#703=20-=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=20DTO=20=EB=B0=8F=20=EB=84=A4?= =?UTF-8?q?=ED=8A=B8=EC=9B=8C=ED=81=AC=20=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WSSiOS/Network/Search/SearchService.swift | 48 +++++++++++++++++++ WSSiOS/Source/Data/DTO/SearchResult.swift | 8 +++- .../Data/Repository/SearchRepository.swift | 15 ++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/WSSiOS/Network/Search/SearchService.swift b/WSSiOS/Network/Search/SearchService.swift index c74919dd0..10ec466e3 100644 --- a/WSSiOS/Network/Search/SearchService.swift +++ b/WSSiOS/Network/Search/SearchService.swift @@ -19,11 +19,59 @@ protocol SearchService { keywordIds: [Int], page: Int, size: Int) -> Single + func getRecentSearches() -> Single<[RecentSearch]> + func deleteRecentSearch(id: Int) -> Single + func deleteAllRecentSearches() -> Single } final class DefaultSearchService: NSObject, Networking { } extension DefaultSearchService: SearchService { + func getRecentSearches() -> Single<[RecentSearch]> { + do { + let request = try makeHTTPRequest(method: .get, + path: URLs.Search.recentSearch, + headers: APIConstants.accessTokenHeader, + body: nil) + NetworkLogger.log(request: request) + return tokenCheckURLSession.rx.data(request: request) + .map { try self.decode(data: $0, to: [RecentSearch].self) } + .asSingle() + } catch { + return Single.error(error) + } + } + + func deleteRecentSearch(id: Int) -> Single { + do { + let request = try makeHTTPRequest(method: .delete, + path: URLs.Search.deleteRecentSearchKeyword(id: id), + headers: APIConstants.accessTokenHeader, + body: nil) + NetworkLogger.log(request: request) + return tokenCheckURLSession.rx.data(request: request) + .map { _ in } + .asSingle() + } catch { + return Single.error(error) + } + } + + func deleteAllRecentSearches() -> Single { + do { + let request = try makeHTTPRequest(method: .delete, + path: URLs.Search.deleteAllRecentSearchKeywords, + headers: APIConstants.accessTokenHeader, + body: nil) + NetworkLogger.log(request: request) + return tokenCheckURLSession.rx.data(request: request) + .map { _ in } + .asSingle() + } catch { + return Single.error(error) + } + } + func getSosopicks() -> Single { do { let request = try makeHTTPRequest(method: .get, diff --git a/WSSiOS/Source/Data/DTO/SearchResult.swift b/WSSiOS/Source/Data/DTO/SearchResult.swift index 09a805e2c..c352e950a 100644 --- a/WSSiOS/Source/Data/DTO/SearchResult.swift +++ b/WSSiOS/Source/Data/DTO/SearchResult.swift @@ -45,10 +45,16 @@ struct SearchNovel: Codable { var interestCount: Int var novelRating: Float var novelRatingCount: Int - + enum CodingKeys: String, CodingKey { case novelId, novelImage, interestCount, novelRating, novelRatingCount case novelTitle = "title" case novelAuthor = "author" } } + +/// 최근 검색어 조회 API +struct RecentSearch: Codable { + let id: Int + let keyword: String +} diff --git a/WSSiOS/Source/Data/Repository/SearchRepository.swift b/WSSiOS/Source/Data/Repository/SearchRepository.swift index 170d18c31..d72ddba3b 100644 --- a/WSSiOS/Source/Data/Repository/SearchRepository.swift +++ b/WSSiOS/Source/Data/Repository/SearchRepository.swift @@ -18,6 +18,9 @@ protocol SearchRepository { upperNovelRating: Float, keywordIds: [Int], page: Int) -> Observable + func getRecentSearches() -> Observable<[RecentSearch]> + func deleteRecentSearch(id: Int) -> Observable + func deleteAllRecentSearches() -> Observable } struct DefaultSearchRepository: SearchRepository { @@ -52,4 +55,16 @@ struct DefaultSearchRepository: SearchRepository { page: page, size: searchSize).asObservable() } + + func getRecentSearches() -> Observable<[RecentSearch]> { + return searchService.getRecentSearches().asObservable() + } + + func deleteRecentSearch(id: Int) -> Observable { + return searchService.deleteRecentSearch(id: id).asObservable() + } + + func deleteAllRecentSearches() -> Observable { + return searchService.deleteAllRecentSearches().asObservable() + } } From 7148915c1a74f38a7d34f4985c57afeb878357a0 Mon Sep 17 00:00:00 2001 From: onesunny2 Date: Thu, 7 May 2026 23:08:08 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[Feat]=20#703=20-=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=20=EB=B7=B0=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NormalSearchRecentView.swift | 84 +++++++++++++++ .../NormalSearchRecentTagCell.swift | 100 ++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift create mode 100644 WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift new file mode 100644 index 000000000..68b86e8b6 --- /dev/null +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift @@ -0,0 +1,84 @@ +// +// NormalSearchRecentView.swift +// WSSiOS +// +// Created by onesunny2 on 5/6/25. +// + +import UIKit + +import SnapKit +import Then + +final class NormalSearchRecentView: UIView { + + //MARK: - Components + + private let titleLabel = UILabel() + let deleteAllButton = UIButton() + let recentTagCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) + + //MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: frame) + + setUI() + setHierarchy() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: - UI + + private func setUI() { + titleLabel.do { + $0.applyWSSFont(.title2, with: StringLiterals.Search.recentSearchTitle) + $0.textColor = .wssBlack + } + + deleteAllButton.do { + $0.setTitle(StringLiterals.Search.deleteAll, for: .normal) + $0.setTitleColor(.wssGray200, for: .normal) + $0.titleLabel?.font = .Body4 + } + + recentTagCollectionView.do { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumInteritemSpacing = 8 + layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) + + $0.collectionViewLayout = layout + $0.isScrollEnabled = true + $0.showsHorizontalScrollIndicator = false + $0.backgroundColor = .clear + } + } + + private func setHierarchy() { + self.addSubviews(titleLabel, deleteAllButton, recentTagCollectionView) + } + + private func setLayout() { + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(8) + $0.leading.equalToSuperview().inset(20) + } + + deleteAllButton.snp.makeConstraints { + $0.centerY.equalTo(titleLabel) + $0.trailing.equalToSuperview().inset(20) + } + + recentTagCollectionView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(12) + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(35) + $0.bottom.equalToSuperview().inset(12) + } + } +} diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift new file mode 100644 index 000000000..4a7444634 --- /dev/null +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift @@ -0,0 +1,100 @@ +// +// NormalSearchRecentTagCell.swift +// WSSiOS +// +// Created by onesunny2 on 5/6/25. +// + +import UIKit + +import RxSwift +import SnapKit +import Then + +final class NormalSearchRecentTagCell: UICollectionViewCell { + + //MARK: - Properties + + var deleteAction: (() -> Void)? + private var disposeBag = DisposeBag() + + //MARK: - Components + + private let keywordLabel = UILabel() + private let deleteButton = UIButton() + private let contentStackView = UIStackView() + + //MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: frame) + + setUI() + setHierarchy() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + deleteAction = nil + disposeBag = DisposeBag() + } + + //MARK: - UI + + private func setUI() { + self.contentView.do { + $0.layer.cornerRadius = 17.5 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.wssPrimary100.cgColor + $0.backgroundColor = .wssWhite + } + + contentStackView.do { + $0.axis = .horizontal + $0.spacing = 6 + $0.alignment = .center + } + + keywordLabel.do { + $0.textColor = .wssPrimary100 + } + + deleteButton.do { + $0.setImage(.icKeywordCancel, for: .normal) + $0.isUserInteractionEnabled = true + } + } + + private func setHierarchy() { + contentView.addSubview(contentStackView) + contentStackView.addArrangedSubviews(keywordLabel, deleteButton) + } + + private func setLayout() { + contentStackView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(13) + $0.centerY.equalToSuperview() + } + + deleteButton.snp.makeConstraints { + $0.size.equalTo(16) + } + } + + //MARK: - Data + + func bindData(keyword: String) { + keywordLabel.applyWSSFont(.body2, with: keyword) + + deleteButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.deleteAction?() + }) + .disposed(by: disposeBag) + } +} From dd8ea336fca3239ec70e884eef69cc4df778e48f Mon Sep 17 00:00:00 2001 From: onesunny2 Date: Thu, 7 May 2026 23:08:11 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[Feat]=20#703=20-=20=EC=9D=BC=EB=B0=98=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=99=94=EB=A9=B4=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NormalSearchView/NormalSearchView.swift | 41 +++++++----- .../NormalSearchViewController.swift | 67 +++++++++++++++++-- .../NormalSearchViewModel.swift | 63 +++++++++++++++-- 3 files changed, 143 insertions(+), 28 deletions(-) diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift index 7a4dead22..72c248ead 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift @@ -11,33 +11,39 @@ import SnapKit import Then final class NormalSearchView: UIView { - + //MARK: - Components - + let headerView = NormalSearchHeaderView() + private let contentStackView = UIStackView() + let recentSearchView = NormalSearchRecentView() let sosoPickView = SearchSosoPickView() let resultView = NormalSearchResultView() let emptyView = NormalSearchEmptyView() - let loadingView = WSSLoadingView() - + // MARK: - Life Cycle - + override init(frame: CGRect) { super.init(frame: frame) - + setUI() setHierarchy() setLayout() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + //MARK: - UI - + private func setUI() { + contentStackView.do { + $0.axis = .vertical + $0.spacing = 32 + } + recentSearchView.isHidden = true resultView.isHidden = true emptyView.isHidden = true loadingView.isHidden = true @@ -45,20 +51,21 @@ final class NormalSearchView: UIView { private func setHierarchy() { self.addSubviews(headerView, - sosoPickView, + contentStackView, resultView, emptyView, loadingView) + contentStackView.addArrangedSubviews(recentSearchView, sosoPickView) } - + private func setLayout() { headerView.snp.makeConstraints { $0.top.equalTo(self.safeAreaLayoutGuide.snp.top).inset(1) $0.leading.trailing.equalToSuperview() } - sosoPickView.snp.makeConstraints { - $0.top.equalTo(headerView.snp.bottom).offset(16) + contentStackView.snp.makeConstraints { + $0.top.equalTo(headerView.snp.bottom).offset(8) $0.leading.trailing.equalToSuperview() } @@ -66,20 +73,20 @@ final class NormalSearchView: UIView { $0.top.equalTo(headerView.snp.bottom) $0.horizontalEdges.bottom.equalToSuperview() } - + emptyView.snp.makeConstraints { $0.top.equalTo(headerView.snp.bottom) $0.horizontalEdges.bottom.equalToSuperview() } - + loadingView.snp.makeConstraints { $0.top.equalTo(headerView.snp.bottom) $0.horizontalEdges.bottom.equalToSuperview() } } - + //MARK: - Custom Methods - + func showLoadingView(isShow: Bool) { loadingView.do { $0.isHidden = !isShow diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift index 31fbc2042..15dfe5674 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift @@ -14,12 +14,15 @@ import RxGesture final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { //MARK: - Properties - + private let viewModel: NormalSearchViewModel private let disposeBag = DisposeBag() - + private let viewWillAppearRelay = PublishRelay() + private let deleteRecentSearchRelay = PublishRelay() + private var currentRecentKeywords: [RecentSearch] = [] + //MARK: - Components - + private let rootView = NormalSearchView() private let emptyView = NormalSearchEmptyView() @@ -40,9 +43,10 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + setNavigationBar() swipeBackGesture() + viewWillAppearRelay.accept(()) } override func viewDidLoad() { @@ -89,10 +93,14 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { rootView.sosoPickView.sosopickCollectionView.register( SosoPickCollectionViewCell.self, forCellWithReuseIdentifier: SosoPickCollectionViewCell.cellIdentifier) + rootView.recentSearchView.recentTagCollectionView.register( + NormalSearchRecentTagCell.self, + forCellWithReuseIdentifier: NormalSearchRecentTagCell.cellIdentifier) } - + private func setDelegate() { rootView.headerView.searchTextField.delegate = self + rootView.recentSearchView.recentTagCollectionView.delegate = self } private func bindViewModel() { @@ -118,7 +126,11 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { normalSearchCellSelected: rootView.resultView.normalSearchCollectionView.rx.itemSelected, reachedBottom: reachedBottom, normalSearchCollectionViewSwipeGesture: collectionViewSwipeGesture, - sosoPickCellSelected: rootView.sosoPickView.sosopickCollectionView.rx.itemSelected) + sosoPickCellSelected: rootView.sosoPickView.sosopickCollectionView.rx.itemSelected, + viewWillAppear: viewWillAppearRelay.asObservable(), + recentSearchTagSelected: rootView.recentSearchView.recentTagCollectionView.rx.itemSelected, + recentSearchDeleteAllButtonDidTap: rootView.recentSearchView.deleteAllButton.rx.tap, + recentSearchDeleteButtonDidTap: deleteRecentSearchRelay.asObservable()) let output = viewModel.transform(from: input, disposeBag: disposeBag) output.resultCount @@ -241,6 +253,34 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { owner.presentInduceLoginViewController() }) .disposed(by: disposeBag) + + output.recentSearchList + .observe(on: MainScheduler.instance) + .do(onNext: { [weak self] list in + self?.currentRecentKeywords = list + }) + .bind(to: rootView.recentSearchView.recentTagCollectionView.rx.items( + cellIdentifier: NormalSearchRecentTagCell.cellIdentifier, + cellType: NormalSearchRecentTagCell.self)) { [weak self] _, recentSearch, cell in + cell.bindData(keyword: recentSearch.keyword) + cell.deleteAction = { self?.deleteRecentSearchRelay.accept(recentSearch.id) } + } + .disposed(by: disposeBag) + + output.showRecentSearchView + .drive(with: self, onNext: { owner, shouldShow in + owner.rootView.recentSearchView.isHidden = !shouldShow + }) + .disposed(by: disposeBag) + + output.fillSearchTextField + .observe(on: MainScheduler.instance) + .subscribe(with: self, onNext: { owner, keyword in + owner.rootView.headerView.searchTextField.text = keyword + owner.rootView.headerView.searchTextField.sendActions(for: .valueChanged) + owner.rootView.headerView.searchTextField.sendActions(for: .editingDidEndOnExit) + }) + .disposed(by: disposeBag) } private func bindAction() { @@ -266,7 +306,7 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { } extension NormalSearchViewController: UITextFieldDelegate { - func textField(_ textField: UITextField, + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let currentText = textField.text ?? "" @@ -274,3 +314,16 @@ extension NormalSearchViewController: UITextFieldDelegate { return newText.count <= 30 } } + +extension NormalSearchViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + guard collectionView == rootView.recentSearchView.recentTagCollectionView, + indexPath.row < currentRecentKeywords.count else { return .zero } + let keyword = currentRecentKeywords[indexPath.row].keyword + let textWidth = (keyword as NSString).size(withAttributes: [.font: UIFont.Body2]).width + let width = ceil(textWidth) + 13 * 2 + 6 + 16 + return CGSize(width: min(width, UIScreen.main.bounds.width - 40), height: 35) + } +} diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift index 129478b4b..616f1a642 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift @@ -37,9 +37,12 @@ final class NormalSearchViewModel: ViewModelType { private let isLogined = APIConstants.isLogined private let sosoPickList = BehaviorRelay<[SosoPickNovel]>(value: []) private let presentToInduceLoginView = PublishRelay() + + // 최근 검색어 + private let recentSearchList = BehaviorRelay<[RecentSearch]>(value: []) //MARK: - Inputs - + struct Input { let searchTextUpdated: ControlProperty let searchTextFieldEditingDidBegin: ControlEvent @@ -54,10 +57,14 @@ final class NormalSearchViewModel: ViewModelType { let reachedBottom: Observable let normalSearchCollectionViewSwipeGesture: Observable let sosoPickCellSelected: ControlEvent + let viewWillAppear: Observable + let recentSearchTagSelected: ControlEvent + let recentSearchDeleteAllButtonDidTap: ControlEvent + let recentSearchDeleteButtonDidTap: Observable } - + //MARK: - Outputs - + struct Output { let resultCount: Observable let normalSearchList: Observable<[SearchNovel]> @@ -73,6 +80,9 @@ final class NormalSearchViewModel: ViewModelType { let showLoadingView: Observable let sosoPickList: Observable<[SosoPickNovel]> let presentToInduceLoginView: Observable + let recentSearchList: Observable<[RecentSearch]> + let showRecentSearchView: Driver + let fillSearchTextField: Observable } //MARK: - init @@ -217,6 +227,48 @@ final class NormalSearchViewModel: ViewModelType { let endEditing = input.normalSearchCollectionViewSwipeGesture .map { _ in () } + // 최근 검색어 로직 + input.viewWillAppear + .filter { self.isLogined } + .flatMapLatest { _ in + self.searchRepository.getRecentSearches() + .catchAndReturn([]) + } + .subscribe(with: self, onNext: { owner, data in + owner.recentSearchList.accept(data) + }) + .disposed(by: disposeBag) + + input.recentSearchDeleteButtonDidTap + .do(onNext: { id in + var list = self.recentSearchList.value + list.removeAll { $0.id == id } + self.recentSearchList.accept(list) + }) + .flatMapLatest { id in + self.searchRepository.deleteRecentSearch(id: id) + .catchAndReturn(()) + } + .subscribe() + .disposed(by: disposeBag) + + input.recentSearchDeleteAllButtonDidTap + .do(onNext: { self.recentSearchList.accept([]) }) + .flatMapLatest { self.searchRepository.deleteAllRecentSearches().catchAndReturn(()) } + .subscribe() + .disposed(by: disposeBag) + + let fillSearchTextField = input.recentSearchTagSelected + .withLatestFrom(recentSearchList) { indexPath, list in list[indexPath.row].keyword } + + let showRecentSearchView = Observable.combineLatest( + recentSearchList.map { !$0.isEmpty }, + input.searchTextUpdated.map { $0.isEmpty }, + normalSearchList.map { $0.isEmpty } + ) + .map { $0 && $1 && $2 } + .asDriver(onErrorJustReturn: false) + return Output(resultCount: resultCount.asObservable(), normalSearchList: normalSearchList.asObservable(), scrollToTop: returnKeyEnabled.asObservable(), @@ -230,6 +282,9 @@ final class NormalSearchViewModel: ViewModelType { endEditing: endEditing, showLoadingView: showLoadingView.asObservable(), sosoPickList: sosoPickList.asObservable(), - presentToInduceLoginView: presentToInduceLoginView.asObservable()) + presentToInduceLoginView: presentToInduceLoginView.asObservable(), + recentSearchList: recentSearchList.asObservable(), + showRecentSearchView: showRecentSearchView, + fillSearchTextField: fillSearchTextField.asObservable()) } } From cbfa64dd44a71a2b07a3258640d7f4227846d7b5 Mon Sep 17 00:00:00 2001 From: onesunny2 Date: Thu, 7 May 2026 23:13:51 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[Chore]=20#703=20-=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=20=EB=B7=B0=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NormalAssistantView/NormalSearchRecentView.swift | 1 + .../NormalSearchCell/NormalSearchRecentTagCell.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift index 68b86e8b6..ec43fcac5 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchRecentView.swift @@ -28,6 +28,7 @@ final class NormalSearchRecentView: UIView { setLayout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift index 4a7444634..f58a75fc4 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchCell/NormalSearchRecentTagCell.swift @@ -34,6 +34,7 @@ final class NormalSearchRecentTagCell: UICollectionViewCell { setLayout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -66,7 +67,6 @@ final class NormalSearchRecentTagCell: UICollectionViewCell { deleteButton.do { $0.setImage(.icKeywordCancel, for: .normal) - $0.isUserInteractionEnabled = true } } From c07ee177cd2f29fc5217526ff2414b7184f89b89 Mon Sep 17 00:00:00 2001 From: onesunny2 Date: Fri, 8 May 2026 20:32:30 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[Chore]=20#703=20-=20=EC=B5=9C=EA=B7=BC?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EA=B0=92=20=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WSSiOS/Network/Search/SearchService.swift | 2 +- WSSiOS/Source/Data/DTO/SearchResult.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/WSSiOS/Network/Search/SearchService.swift b/WSSiOS/Network/Search/SearchService.swift index 10ec466e3..fc98603ed 100644 --- a/WSSiOS/Network/Search/SearchService.swift +++ b/WSSiOS/Network/Search/SearchService.swift @@ -35,7 +35,7 @@ extension DefaultSearchService: SearchService { body: nil) NetworkLogger.log(request: request) return tokenCheckURLSession.rx.data(request: request) - .map { try self.decode(data: $0, to: [RecentSearch].self) } + .map { try self.decode(data: $0, to: RecentSearches.self).recentSearches } .asSingle() } catch { return Single.error(error) diff --git a/WSSiOS/Source/Data/DTO/SearchResult.swift b/WSSiOS/Source/Data/DTO/SearchResult.swift index c352e950a..33e1ada07 100644 --- a/WSSiOS/Source/Data/DTO/SearchResult.swift +++ b/WSSiOS/Source/Data/DTO/SearchResult.swift @@ -54,6 +54,10 @@ struct SearchNovel: Codable { } /// 최근 검색어 조회 API +struct RecentSearches: Codable { + let recentSearches: [RecentSearch] +} + struct RecentSearch: Codable { let id: Int let keyword: String