From c2972ed59fc5241c1d0e9433baad5be5b4b64952 Mon Sep 17 00:00:00 2001 From: goghrf Date: Wed, 27 Nov 2024 21:14:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=ED=83=AD=20?= =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EA=B2=80=EC=83=89=20UI=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 --- .../Repository/Post/PostRepositoryImpl.swift | 18 +++ .../Post/PostRepositoryMockImpl.swift | 19 ++- .../RepositoryInterface/PostRepository.swift | 1 + .../Sources/Domain/UseCase/PostUseCase.swift | 16 ++- .../postDeleteBtn.imageset/Contents.json | 21 +++ .../Icons/postDeleteBtn.imageset/Vector.svg | 3 + .../Coordinator/SearchCoordinator.swift | 6 +- .../Presentation/Search/SearchViewModel.swift | 51 +++++++- .../View/SearchGrid/SearchGridView.swift | 2 +- .../View/SearchResult/SearchResultCVC.swift | 109 ++++++++++++++++ .../View/SearchResult/SearchResultView.swift | 121 +++++++++++++++++- .../SearchGridViewController.swift | 2 +- .../SearchResultViewController.swift | 13 ++ .../Presentation/Utils/Constants.swift | 4 + 14 files changed, 366 insertions(+), 20 deletions(-) create mode 100644 MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Contents.json create mode 100644 MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Vector.svg create mode 100644 MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift diff --git a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift index 38a7eab..21eade6 100644 --- a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift +++ b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryImpl.swift @@ -229,4 +229,22 @@ public class PostRepositoryImpl: PostRepository { ] return Just(posts).eraseToAnyPublisher() } + + public func getRecentSearchPostData() -> AnyPublisher<[Post], Never> { + let posts: [Post] = [ + Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), + Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8"), + Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") + ] + return Just(posts).eraseToAnyPublisher() + } } diff --git a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift index 5c2a09a..a247d12 100644 --- a/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift +++ b/MyLuxury/Data/Sources/Data/Repository/Post/PostRepositoryMockImpl.swift @@ -179,7 +179,6 @@ public class PostRepositoryMockImpl: PostRepository { ) ] - return Just(postData.filter { $0.post_id == postId }.first!).eraseToAnyPublisher() } @@ -228,4 +227,22 @@ public class PostRepositoryMockImpl: PostRepository { ] return Just(posts).eraseToAnyPublisher() } + + public func getRecentSearchPostData() -> AnyPublisher<[Post], Never> { + let posts: [Post] = [ + Post(post_id: "1", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까???", postThumbnailImage: "testImage1"), + Post(post_id: "2", postUIType: .normal, postCategory: .art, postTitle: "그때 그 시절, 무엇을 하며 놀았을까요??", postThumbnailImage: "testImage2"), + Post(post_id: "3", postUIType: .normal, postCategory: .art, postTitle: "그래서 이 아저씨가 누군데?", postThumbnailImage: "testImage7"), + Post(post_id: "4", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage3"), + Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8"), + Post(post_id: "5", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage5"), + Post(post_id: "6", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage4"), + Post(post_id: "7", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage6"), + Post(post_id: "8", postUIType: .normal, postCategory: .art, postTitle: "흑백 사진은 언제 처음 사용되었을까?", postThumbnailImage: "testImage8") + ] + return Just(posts).eraseToAnyPublisher() + } } diff --git a/MyLuxury/Domain/Sources/Domain/RepositoryInterface/PostRepository.swift b/MyLuxury/Domain/Sources/Domain/RepositoryInterface/PostRepository.swift index 137b074..96b8d30 100644 --- a/MyLuxury/Domain/Sources/Domain/RepositoryInterface/PostRepository.swift +++ b/MyLuxury/Domain/Sources/Domain/RepositoryInterface/PostRepository.swift @@ -12,4 +12,5 @@ public protocol PostRepository { func getHomeViewData() -> AnyPublisher func getPostOneData(postId: String) -> AnyPublisher func getSearchGridPostData() -> AnyPublisher<[Post], Never> + func getRecentSearchPostData() -> AnyPublisher<[Post], Never> } diff --git a/MyLuxury/Domain/Sources/Domain/UseCase/PostUseCase.swift b/MyLuxury/Domain/Sources/Domain/UseCase/PostUseCase.swift index dbb1b44..eb07285 100644 --- a/MyLuxury/Domain/Sources/Domain/UseCase/PostUseCase.swift +++ b/MyLuxury/Domain/Sources/Domain/UseCase/PostUseCase.swift @@ -9,14 +9,17 @@ import Combine public protocol PostUseCase { var postRepository: PostRepository { get } - + /// 홈 게시물 조회 func getHomeViewData() -> AnyPublisher + /// 개별 게시물 조회 func getPostOneData(postId: String) -> AnyPublisher + /// 검색 탭 그리드 게시물 전체 조회 func getSearchGridPostsData() -> AnyPublisher<[Post], Never> + /// 최근 검색 게시물 조회 + func getRecentSearchPostData() -> AnyPublisher<[Post], Never> } public class PostUseCaseImpl: PostUseCase { - public var postRepository: PostRepository private var cancellables = Set() @@ -29,7 +32,7 @@ public class PostUseCaseImpl: PostUseCase { print("PostUseCase deinit") } - /// 홈 화면 데이터 조회 + /// 홈 게시물 조회 public func getHomeViewData() -> AnyPublisher { return postRepository.getHomeViewData().eraseToAnyPublisher() } @@ -39,8 +42,13 @@ public class PostUseCaseImpl: PostUseCase { return postRepository.getPostOneData(postId: postId) } - /// 검색 화면 그리드 게시물 조회 + /// 검색 탭 그리드 게시물 전체 조회 public func getSearchGridPostsData() -> AnyPublisher<[Post], Never> { return postRepository.getSearchGridPostData() } + + /// 최근 검색 게시물 조회 + public func getRecentSearchPostData() -> AnyPublisher<[Post], Never> { + return postRepository.getRecentSearchPostData() + } } diff --git a/MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Contents.json b/MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Contents.json new file mode 100644 index 0000000..fdf1b91 --- /dev/null +++ b/MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Vector.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Vector.svg b/MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Vector.svg new file mode 100644 index 0000000..3841133 --- /dev/null +++ b/MyLuxury/MyLuxury/Assets.xcassets/Icons/postDeleteBtn.imageset/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/MyLuxury/Presentation/Sources/Presentation/Coordinator/SearchCoordinator.swift b/MyLuxury/Presentation/Sources/Presentation/Coordinator/SearchCoordinator.swift index 14b9570..f1bfcc5 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Coordinator/SearchCoordinator.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Coordinator/SearchCoordinator.swift @@ -52,14 +52,12 @@ public class SearchCoordinatorImpl: SearchCoordinator, @preconcurrency SearchGri func goToSearchResultView(searchVM: SearchViewModel) { let searchResultVC = SearchResultViewController(searchVM: searchVM) searchResultVC.delegate = self - searchResultVC.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - searchResultVC.modalPresentationStyle = .overCurrentContext - self.navigationController.present(searchResultVC, animated: true) + self.navigationController.pushViewController(searchResultVC, animated: true) } @MainActor func goBackToResultGridView() { - self.navigationController.dismiss(animated: true) + self.navigationController.popViewController(animated: true) } @MainActor diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/SearchViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Search/SearchViewModel.swift index eddd986..55f0605 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/SearchViewModel.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/SearchViewModel.swift @@ -16,6 +16,7 @@ public class SearchViewModel { var cancellables = Set() var searchGridPosts: [Post] = [] + var recentSearchPosts: [Post] = [] init(postUseCase: PostUseCase) { print("SearchViewModel init") @@ -37,8 +38,16 @@ public class SearchViewModel { self.output.send(.goBackToSearchResultView) case .searchGridViewLoaded: getSearchGridPosts() - case .postTapped(let post): - self.output.send(.goToPostView(post)) + case .postTappedFromGrid(let post): + self.output.send(.goToPostViewFromGrid(post)) + case .postTappedFromRecentSearch(let post): + self.output.send(.goToPostViewFromSearch(post)) + case .searchResultViewLoaded: + getRecentSearchPosts() + case .deleteRecentSearchPostBtnTapped(let index): + self.output.send(.removeRecentSearchPost(index)) + case .searchResultViewDisappeared: + saveRecentSearchPosts() } }.store(in: &cancellables) return output.eraseToAnyPublisher() @@ -52,8 +61,16 @@ public class SearchViewModel { self.input.send(.searchBarCancelTapped) case .searchGridViewLoaded: self.input.send(.searchGridViewLoaded) - case .postTapped(let post): - self.input.send(.postTapped(post)) + case .postTappedFromGrid(let post): + self.input.send(.postTappedFromGrid(post)) + case .postTappedFromRecentSearch(let post): + self.input.send(.postTappedFromRecentSearch(post)) + case .searchResultViewLoaded: + self.input.send(.searchResultViewLoaded) + case .deleteRecentSearchPostBtnTapped(let index): + self.input.send(.deleteRecentSearchPostBtnTapped(index)) + case .searchResultViewDisappeared: + self.input.send(.searchResultViewDisappeared) } } @@ -72,6 +89,20 @@ public class SearchViewModel { self.output.send(.getSearchGridPosts) }.store(in: &cancellables) } + + private func getRecentSearchPosts() { + postUseCase.getRecentSearchPostData() + .sink { [weak self] recentSearchPostData in + guard let self = self else { return } + self.recentSearchPosts = recentSearchPostData + self.output.send(.getRecentSearchPosts) + }.store(in: &cancellables) + } + + private func saveRecentSearchPosts() { + print("최근 검색 기록 저장 api 호출중") + // self.output.send(.saveRecentSearchPosts) + } } extension SearchViewModel { @@ -79,12 +110,20 @@ extension SearchViewModel { case searchBarTapped case searchBarCancelTapped case searchGridViewLoaded - case postTapped(Post) + case postTappedFromGrid(Post) + case searchResultViewLoaded + case postTappedFromRecentSearch(Post) + case deleteRecentSearchPostBtnTapped(Int) + case searchResultViewDisappeared } enum Output { case goToSearchResultView case goBackToSearchResultView case getSearchGridPosts - case goToPostView(Post) + case goToPostViewFromGrid(Post) + case getRecentSearchPosts + case goToPostViewFromSearch(Post) + case removeRecentSearchPost(Int) + case saveRecentSearchPosts } } diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift index 790bfcc..8c31351 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchGrid/SearchGridView.swift @@ -120,7 +120,7 @@ extension SearchGridView: UICollectionViewDataSource, UICollectionViewDelegateFl func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let post = posts[indexPath.row] - searchVM.sendInputEvent(input: .postTapped(post)) + searchVM.sendInputEvent(input: .postTappedFromGrid(post)) } /// 각 셀의 크기를 동적으로 지정 diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift new file mode 100644 index 0000000..bd6ec0e --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultCVC.swift @@ -0,0 +1,109 @@ +// +// SearchResultCVC.swift +// Presentation +// +// Created by KoSungmin on 11/26/24. +// + +import UIKit +import Domain + +final class SearchResultCVC: UICollectionViewCell { + static let identifier = "SearchResultCVC" + + private let postThumbnailImageView: UIImageView = { + let imageView = UIImageView() + return imageView + }() + + private let postTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.regular, size: 16) + label.textColor = .white + return label + }() + + private let postCategoryLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.light, size: 14) + label.textColor = UIColor.getCustomColor(.textGray) + return label + }() + + private var recentPostDeleteBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setImage(UIImage(named: "postDeleteBtn"), for: .normal) + btn.frame = CGRect(x: 0, y: 0, width: 20, height: 20) + btn.addTarget(self, action: #selector(deleteRecentPost), for: .touchUpInside) + return btn + }() + + var thumbnailImage: String? { + didSet { + postThumbnailImageView.image = UIImage(named: thumbnailImage ?? "blackScreen") + } + } + + var postTitle: String? { + didSet { + postTitleLabel.text = postTitle + } + } + + var postCategory: String? { + didSet { + postCategoryLabel.text = postCategory + } + } + + /// 최근 검색일 경우에는 삭제 버튼이 보이지 않도록 + var isRecentPost: Bool = true { + didSet { + recentPostDeleteBtn.isHidden = !isRecentPost + } + } + + /// 최근 검색 게시물 개별 삭제 버튼이 눌렸을 때 호출되는 클로저 + var onDeleteRecentSearchPost: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUpHierarchy() { + self.addSubview(postThumbnailImageView) + self.addSubview(postTitleLabel) + self.addSubview(postCategoryLabel) + self.addSubview(recentPostDeleteBtn) + } + + private func setUpLayout() { + self.postThumbnailImageView.translatesAutoresizingMaskIntoConstraints = false + self.postTitleLabel.translatesAutoresizingMaskIntoConstraints = false + self.postCategoryLabel.translatesAutoresizingMaskIntoConstraints = false + self.recentPostDeleteBtn.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + postThumbnailImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), + postThumbnailImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + postThumbnailImageView.widthAnchor.constraint(equalToConstant: searchPostResultCVCThumnbnailImageViewLength), + postThumbnailImageView.heightAnchor.constraint(equalToConstant: searchPostResultCVCThumnbnailImageViewLength), + postTitleLabel.leadingAnchor.constraint(equalTo: postThumbnailImageView.trailingAnchor, constant: 10), + postTitleLabel.bottomAnchor.constraint(equalTo: self.centerYAnchor, constant: -2.5), + postCategoryLabel.leadingAnchor.constraint(equalTo: postThumbnailImageView.trailingAnchor, constant: 10), + postCategoryLabel.topAnchor.constraint(equalTo: self.centerYAnchor, constant: 5), + recentPostDeleteBtn.centerYAnchor.constraint(equalTo: self.centerYAnchor), + recentPostDeleteBtn.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15) + ]) + } + + @objc + private func deleteRecentPost() { + self.onDeleteRecentSearchPost?() + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift index 1eb4247..f189d7f 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/View/SearchResult/SearchResultView.swift @@ -9,8 +9,24 @@ import UIKit import Combine import Domain -final class SearchResultView: UIView, UITextFieldDelegate { +final class SearchResultView: UIView { + private var cancellables = Set() private let searchVM: SearchViewModel + /// 최근 검색인지 검색 결과인지 여부 + @Published private var isRecentSearch: Bool = true + + /// 최근 검색 + var recentSearchPosts: [Post] = [] { + didSet { + resultCollectionView.reloadData() + } + } + /// 검색 결과 + var searchResultPosts: [Post] = [] { + didSet { + resultCollectionView.reloadData() + } + } lazy var searchTextField: UITextField = { let textField = UITextField() @@ -37,7 +53,6 @@ final class SearchResultView: UIView, UITextFieldDelegate { }() private let cancelTextLabel: UILabel = { - let label = UILabel() label.text = "취소" label.font = UIFont.pretendard(.light, size: 18) @@ -46,11 +61,32 @@ final class SearchResultView: UIView, UITextFieldDelegate { return label }() + /// 텍스트 필드가 비어있을 경우 최근 검색, 비어있지 않을 경우 검색 결과 + private let resultTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.bold, size: 18) + label.textColor = .white + label.text = "최근 검색" + return label + }() + + /// 텍스트 필드에 입력값이 없을 때는 최근 검색 + /// 입력값이 있을 때는 검색 결과 + private let resultCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.itemSize = CGSize(width: screenWidth, height: searchPostResultCVCHeight) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + return collectionView + }() + init(searchVM: SearchViewModel) { self.searchVM = searchVM super.init(frame: .zero) self.backgroundColor = .black self.searchTextField.delegate = self + bindEvent() + setUpCollectionView() setUpHierarchy() setUpLayout() setUpCancelTextTapGesture() @@ -65,21 +101,47 @@ final class SearchResultView: UIView, UITextFieldDelegate { fatalError("init(coder:) has not been implemented") } + private func bindEvent() { + $isRecentSearch + .receive(on: DispatchQueue.main) + .sink { [weak self] isRecent in + guard let self = self else { return } + self.resultTitleLabel.text = isRecent ? "최근 검색" : "검색 결과" + self.resultCollectionView.reloadData() + }.store(in: &cancellables) + } + + private func setUpCollectionView() { + self.resultCollectionView.dataSource = self + self.resultCollectionView.delegate = self + self.resultCollectionView.register(SearchResultCVC.self, forCellWithReuseIdentifier: "SearchResultCVC") + } + private func setUpHierarchy() { self.addSubview(searchTextField) self.addSubview(cancelTextLabel) + self.addSubview(resultTitleLabel) + self.addSubview(resultCollectionView) } private func setUpLayout() { searchTextField.translatesAutoresizingMaskIntoConstraints = false cancelTextLabel.translatesAutoresizingMaskIntoConstraints = false + resultTitleLabel.translatesAutoresizingMaskIntoConstraints = false + resultCollectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ searchTextField.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 25), searchTextField.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), searchTextField.widthAnchor.constraint(equalToConstant: screenWidth - 70), searchTextField.heightAnchor.constraint(equalToConstant: 45), cancelTextLabel.centerYAnchor.constraint(equalTo: self.searchTextField.centerYAnchor), - cancelTextLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15) + cancelTextLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15), + resultTitleLabel.topAnchor.constraint(equalTo: self.searchTextField.bottomAnchor, constant: 25), + resultTitleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), + resultCollectionView.topAnchor.constraint(equalTo: resultTitleLabel.bottomAnchor, constant: 15), + resultCollectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + resultCollectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + resultCollectionView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor) ]) } @@ -90,6 +152,8 @@ final class SearchResultView: UIView, UITextFieldDelegate { private func setUpDismissKeyboardGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + /// 이 제스처가 다른 터치 이벤트를 막지 않도록 설정 + tapGesture.cancelsTouchesInView = false self.addGestureRecognizer(tapGesture) } @@ -103,3 +167,54 @@ final class SearchResultView: UIView, UITextFieldDelegate { searchTextField.resignFirstResponder() } } + +// MARK: - UITextFieldDelegate +extension SearchResultView: UITextFieldDelegate { + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let currentText = textField.text ?? "" + let updateText = (currentText as NSString).replacingCharacters(in: range, with: string) + isRecentSearch = updateText.isEmpty + return true + } +} +// MARK: - UICollectionViewDelegate +extension SearchResultView: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return isRecentSearch ? recentSearchPosts.count : searchResultPosts.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchResultCVC", for: indexPath) as! SearchResultCVC + if isRecentSearch { + let post = recentSearchPosts[indexPath.row] + cell.isRecentPost = true + cell.postCategory = post.postCategory.tagName + cell.thumbnailImage = post.postThumbnailImage + cell.postTitle = post.postTitle + cell.onDeleteRecentSearchPost = { [weak self] in + guard let self = self else { return } + self.searchVM.sendInputEvent(input: .deleteRecentSearchPostBtnTapped(indexPath.row)) + } + } else { + cell.isRecentPost = false + cell.postCategory = "#인문" + cell.thumbnailImage = "testImage3" + cell.postTitle = "그래서 이 아저씨가 누군데?" + } + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if isRecentSearch { + let post = recentSearchPosts[indexPath.row] + searchVM.sendInputEvent(input: .postTappedFromRecentSearch(post)) + } else { + + } + } + + /// 컬렉션뷰의 스크롤이 시작될 때 호출되는 메소드 + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + dismissKeyboard() + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift index 94557c8..c4b7d85 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchGridViewController.swift @@ -60,7 +60,7 @@ class SearchGridViewController: UIViewController { self.delegate?.goToSearchResultView(searchVM: self.searchVM) case .getSearchGridPosts: self.rootView.posts = self.searchVM.searchGridPosts - case .goToPostView(let post): + case .goToPostViewFromGrid(let post): self.delegate?.goToPostView(post: post) default: break diff --git a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift index 2f63176..002723b 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Search/ViewController/SearchResultViewController.swift @@ -11,6 +11,7 @@ import Combine protocol SearchResultViewControllerDelegate: AnyObject { func goBackToResultGridView() + func goToPostView(post: Post) } class SearchResultViewController: UIViewController { @@ -45,6 +46,12 @@ class SearchResultViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + searchVM.sendInputEvent(input: .searchResultViewLoaded) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + searchVM.sendInputEvent(input: .searchResultViewDisappeared) } private func bindData() { @@ -58,6 +65,12 @@ class SearchResultViewController: UIViewController { switch event { case .goBackToSearchResultView: self.delegate?.goBackToResultGridView() + case .getRecentSearchPosts: + self.rootView.recentSearchPosts = self.searchVM.recentSearchPosts + case .goToPostViewFromSearch(let post): + self.delegate?.goToPostView(post: post) + case .removeRecentSearchPost(let index): + self.rootView.recentSearchPosts.remove(at: index) default: break } diff --git a/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift b/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift index 516eed8..9e62d45 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift @@ -27,3 +27,7 @@ import UIKit @MainActor let homeEditorRecommendCVCLength = screenWidth - 30 /// 게시물 인디케이터 높이 @MainActor let postIndicatorHeight = screenHeight / 35 +/// 최근 검색 및 검색 결과 컬렉션뷰셀 높이 +@MainActor let searchPostResultCVCHeight = screenWidth / 6 +/// 최근 검색 및 검색 결과 컬렉션뷰셀 내 썸네일 이미지 변의 길이 +@MainActor let searchPostResultCVCThumnbnailImageViewLength = screenHeight / 17.5 From 258b2c22da6882f7d5a1e19544ff48e383648486 Mon Sep 17 00:00:00 2001 From: goghrf Date: Thu, 28 Nov 2024 21:32:15 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80(=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC)=20=EB=A9=94=EC=9D=B8=20=ED=99=94=EB=A9=B4=20UI=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 --- .../Icons/chevronRight.imageset/Contents.json | 21 ++++ .../Icons/chevronRight.imageset/Vector-3.svg | 3 + .../Presentation/Library/LibraryView.swift | 51 ---------- .../Library/View/LibraryCVC.swift | 61 ++++++++++++ .../Library/View/LibraryHeaderView.swift | 42 ++++++++ .../Library/View/LibraryItems.swift | 18 ++++ .../Library/View/LibraryView.swift | 98 +++++++++++++++++++ .../LibraryViewController.swift | 0 .../{ => ViewModel}/LibraryViewModel.swift | 0 .../Presentation/Utils/Constants.swift | 2 + 10 files changed, 245 insertions(+), 51 deletions(-) create mode 100644 MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Contents.json create mode 100644 MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Vector-3.svg delete mode 100644 MyLuxury/Presentation/Sources/Presentation/Library/LibraryView.swift create mode 100644 MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryCVC.swift create mode 100644 MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryHeaderView.swift create mode 100644 MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryItems.swift create mode 100644 MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryView.swift rename MyLuxury/Presentation/Sources/Presentation/Library/{ => ViewController}/LibraryViewController.swift (100%) rename MyLuxury/Presentation/Sources/Presentation/Library/{ => ViewModel}/LibraryViewModel.swift (100%) diff --git a/MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Contents.json b/MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Contents.json new file mode 100644 index 0000000..b6b8e12 --- /dev/null +++ b/MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Vector-3.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Vector-3.svg b/MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Vector-3.svg new file mode 100644 index 0000000..b703ba3 --- /dev/null +++ b/MyLuxury/MyLuxury/Assets.xcassets/Icons/chevronRight.imageset/Vector-3.svg @@ -0,0 +1,3 @@ + + + diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryView.swift b/MyLuxury/Presentation/Sources/Presentation/Library/LibraryView.swift deleted file mode 100644 index 46e1c0c..0000000 --- a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryView.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// LibraryView.swift -// Presentation -// -// Created by KoSungmin on 11/21/24. -// - -import UIKit - -final class LibraryView: UIView { - private let libraryVM: LibraryViewModel - - private let logoutBtn: UIButton = { - let btn = UIButton() - btn.setTitle("로그아웃", for: .normal) - btn.setTitleColor(.systemBlue, for: .normal) - btn.backgroundColor = .lightGray - btn.addTarget(self, action: #selector(logout), for: .touchUpInside) - return btn - }() - - init(libraryVM: LibraryViewModel) { - self.libraryVM = libraryVM - super.init(frame: .zero) - setUpHierarchy() - setUpLayout() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setUpHierarchy() { - self.addSubview(logoutBtn) - } - - private func setUpLayout() { - logoutBtn.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - logoutBtn.centerXAnchor.constraint(equalTo: self.centerXAnchor), - logoutBtn.centerYAnchor.constraint(equalTo: centerYAnchor), - logoutBtn.widthAnchor.constraint(equalToConstant: 100), - logoutBtn.heightAnchor.constraint(equalToConstant: 50) - ]) - } - - @objc - private func logout() { - libraryVM.sendInputEvent(input: .logoutBtnTapped) - } -} diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryCVC.swift b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryCVC.swift new file mode 100644 index 0000000..38ec451 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryCVC.swift @@ -0,0 +1,61 @@ +// +// LibraryCVC.swift +// Presentation +// +// Created by KoSungmin on 11/28/24. +// + +import UIKit + +final class LibraryCVC: UICollectionViewCell { + static let idenrifier = "LibraryCVC" + + private let itemTitleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.bold, size: 20) + label.textColor = .white + return label + }() + + private let goToNextImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "chevronRight") + imageView.frame = CGRect(x: 0, y: 0, width: 10, height: 18) + return imageView + }() + + var itemTitle: String? { + didSet { + self.itemTitleLabel.text = itemTitle + if itemTitle == "로그아웃" || itemTitle == "회원탈퇴" { + goToNextImageView.isHidden = true + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUpHierarchy() { + self.addSubview(itemTitleLabel) + self.addSubview(goToNextImageView) + } + + private func setUpLayout() { + itemTitleLabel.translatesAutoresizingMaskIntoConstraints = false + goToNextImageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + itemTitleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + itemTitleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15), + goToNextImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + goToNextImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15) + ]) + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryHeaderView.swift b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryHeaderView.swift new file mode 100644 index 0000000..54ecf37 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryHeaderView.swift @@ -0,0 +1,42 @@ +// +// LibraryHeaderView.swift +// Presentation +// +// Created by KoSungmin on 11/27/24. +// + +import UIKit + +final class LibraryHeaderView: UIView { + private let libraryTextLabel: UILabel = { + let label = UILabel() + label.font = UIFont.pretendard(.bold, size: 32) + label.textColor = .white + label.text = "마이 페이지" + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setUpHierarchy() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUpHierarchy() { + self.addSubview(libraryTextLabel) + } + + private func setUpLayout() { + libraryTextLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.widthAnchor.constraint(equalToConstant: screenWidth), + self.heightAnchor.constraint(equalToConstant: navigationBarHeight), + libraryTextLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + libraryTextLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15) + ]) + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryItems.swift b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryItems.swift new file mode 100644 index 0000000..1a152ef --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryItems.swift @@ -0,0 +1,18 @@ +// +// LibraryItems.swift +// Presentation +// +// Created by KoSungmin on 11/28/24. +// + +import UIKit + +enum LibraryItems: String, CaseIterable { + case setCategories = "관심 카테고리 설정" + case recentPosts = "최근 본 지식" + case savedPosts = "저장한 지식" + case serviceTerms = "서비스 이용약관" + case personalInfoGuide = "개인정보 처리방침" + case logout = "로그아웃" + case widthdraw = "회원탈퇴" +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryView.swift b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryView.swift new file mode 100644 index 0000000..39624c4 --- /dev/null +++ b/MyLuxury/Presentation/Sources/Presentation/Library/View/LibraryView.swift @@ -0,0 +1,98 @@ +// +// LibraryView.swift +// Presentation +// +// Created by KoSungmin on 11/21/24. +// + +import UIKit + +final class LibraryView: UIView { + private let libraryVM: LibraryViewModel + let headerView = LibraryHeaderView() + + let collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = CGSize(width: screenWidth, height: libraryCVCHeight) + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 15 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + collectionView.showsVerticalScrollIndicator = false + return collectionView + }() + + private let logoutBtn: UIButton = { + let btn = UIButton() + btn.setTitle("로그아웃", for: .normal) + btn.setTitleColor(.systemBlue, for: .normal) + btn.backgroundColor = .lightGray + btn.addTarget(self, action: #selector(logout), for: .touchUpInside) + return btn + }() + + init(libraryVM: LibraryViewModel) { + self.libraryVM = libraryVM + super.init(frame: .zero) + self.backgroundColor = .black + setUpCollectionView() + setUpHierarchy() + setUpLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUpCollectionView() { + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(LibraryCVC.self, forCellWithReuseIdentifier: "LibraryCVC") + } + + private func setUpHierarchy() { + self.addSubview(headerView) + self.addSubview(collectionView) + } + + private func setUpLayout() { + headerView.translatesAutoresizingMaskIntoConstraints = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + headerView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor), + headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + collectionView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 15), + collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor), + ]) + } + + @objc + private func logout() { + libraryVM.sendInputEvent(input: .logoutBtnTapped) + } +} + +extension LibraryView: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return LibraryItems.allCases.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let item = LibraryItems.allCases[indexPath.row] + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LibraryCVC", for: indexPath) as! LibraryCVC + cell.itemTitle = item.rawValue + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + switch indexPath.row { + case 5: // 로그아웃 + logout() + default: + break + } + } +} diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewController.swift b/MyLuxury/Presentation/Sources/Presentation/Library/ViewController/LibraryViewController.swift similarity index 100% rename from MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewController.swift rename to MyLuxury/Presentation/Sources/Presentation/Library/ViewController/LibraryViewController.swift diff --git a/MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewModel.swift b/MyLuxury/Presentation/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift similarity index 100% rename from MyLuxury/Presentation/Sources/Presentation/Library/LibraryViewModel.swift rename to MyLuxury/Presentation/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift diff --git a/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift b/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift index 9e62d45..6cea547 100644 --- a/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift +++ b/MyLuxury/Presentation/Sources/Presentation/Utils/Constants.swift @@ -31,3 +31,5 @@ import UIKit @MainActor let searchPostResultCVCHeight = screenWidth / 6 /// 최근 검색 및 검색 결과 컬렉션뷰셀 내 썸네일 이미지 변의 길이 @MainActor let searchPostResultCVCThumnbnailImageViewLength = screenHeight / 17.5 +/// 라이브러리탭 컬렉션뷰셀 높이 +@MainActor let libraryCVCHeight = screenHeight / 19.5