diff --git a/WSSiOS.xcodeproj/project.pbxproj b/WSSiOS.xcodeproj/project.pbxproj index cc5ea0704..fdce52ad9 100644 --- a/WSSiOS.xcodeproj/project.pbxproj +++ b/WSSiOS.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026040801; + CURRENT_PROJECT_VERSION = 2026051101; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 9SVDHQS4M3; GENERATE_INFOPLIST_FILE = YES; @@ -356,7 +356,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.7.0; PRODUCT_BUNDLE_IDENTIFIER = kr.websoso.debug2; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -383,7 +383,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026040801; + CURRENT_PROJECT_VERSION = 2026051101; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 9SVDHQS4M3; GENERATE_INFOPLIST_FILE = YES; @@ -401,7 +401,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.7.0; PRODUCT_BUNDLE_IDENTIFIER = kr.websoso; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/WSSiOS/Network/Search/SearchService.swift b/WSSiOS/Network/Search/SearchService.swift index 71e7b4915..deeac223a 100644 --- a/WSSiOS/Network/Search/SearchService.swift +++ b/WSSiOS/Network/Search/SearchService.swift @@ -14,7 +14,8 @@ protocol SearchService { func searchNormalNovels(query: String, page: Int, size: Int) -> Single func searchDetailNovels(genres: [String], isCompleted: Bool?, - novelRating: Float?, + lowerNovelRating: Float, + upperNovelRating: Float, keywordIds: [Int], page: Int, size: Int) -> Single @@ -70,7 +71,8 @@ extension DefaultSearchService: SearchService { func searchDetailNovels(genres: [String], isCompleted: Bool?, - novelRating: Float?, + lowerNovelRating: Float, + upperNovelRating: Float, keywordIds: [Int], page: Int, size: Int) -> Single { @@ -79,17 +81,15 @@ extension DefaultSearchService: SearchService { URLQueryItem(name: "genres", value: genres.joined(separator: ",")), URLQueryItem(name: "keywordIds", value: keywordIds.map { String($0) }.joined(separator: ",")), URLQueryItem(name: "page", value: String(page)), - URLQueryItem(name: "size", value: String(size)) + URLQueryItem(name: "size", value: String(size)), + URLQueryItem(name: "novelRatingStart", value: String(lowerNovelRating)), + URLQueryItem(name: "novelRatingEnd", value: String(upperNovelRating)) ] if let isCompleted = isCompleted { detailSearchQueryItems.append(URLQueryItem(name: "isCompleted", value: String(isCompleted))) } - - if let novelRating = novelRating { - detailSearchQueryItems.append(URLQueryItem(name: "novelRating", value: String(novelRating))) - } - + do { let request = try makeHTTPRequest(method: .get, path: URLs.Search.detailSearch, diff --git a/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/Contents.json b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/Contents.json new file mode 100644 index 000000000..dc157e09b --- /dev/null +++ b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "imgHomeDetailSearch.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "imgHomeDetailSearch@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "imgHomeDetailSearch@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch.png b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch.png new file mode 100644 index 000000000..b0a67856e Binary files /dev/null and b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch.png differ diff --git a/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch@2x.png b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch@2x.png new file mode 100644 index 000000000..47157e95b Binary files /dev/null and b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch@2x.png differ diff --git a/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch@3x.png b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch@3x.png new file mode 100644 index 000000000..d26e8b69f Binary files /dev/null and b/WSSiOS/Resource/Assets.xcassets/Image/imgHomeDetailSearch.imageset/imgHomeDetailSearch@3x.png differ diff --git a/WSSiOS/Resource/Assets.xcassets/icon/AttractivePoint/icAttractiveWritingSkill.imageset/Contents.json b/WSSiOS/Resource/Assets.xcassets/icon/AttractivePoint/icAttractiveWritingSkill.imageset/Contents.json new file mode 100644 index 000000000..981a43f40 --- /dev/null +++ b/WSSiOS/Resource/Assets.xcassets/icon/AttractivePoint/icAttractiveWritingSkill.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icAttractiveWritingSkill.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WSSiOS/Resource/Assets.xcassets/icon/AttractivePoint/icAttractiveWritingSkill.imageset/icAttractiveWritingSkill.svg b/WSSiOS/Resource/Assets.xcassets/icon/AttractivePoint/icAttractiveWritingSkill.imageset/icAttractiveWritingSkill.svg new file mode 100644 index 000000000..56c18567b --- /dev/null +++ b/WSSiOS/Resource/Assets.xcassets/icon/AttractivePoint/icAttractiveWritingSkill.imageset/icAttractiveWritingSkill.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/WSSiOS/Resource/Assets.xcassets/icon/icBookPlus.imageset/Contents.json b/WSSiOS/Resource/Assets.xcassets/icon/icBookPlus.imageset/Contents.json new file mode 100644 index 000000000..1b21a811f --- /dev/null +++ b/WSSiOS/Resource/Assets.xcassets/icon/icBookPlus.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "mdi_book-plus-outline.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WSSiOS/Resource/Assets.xcassets/icon/icBookPlus.imageset/mdi_book-plus-outline.svg b/WSSiOS/Resource/Assets.xcassets/icon/icBookPlus.imageset/mdi_book-plus-outline.svg new file mode 100644 index 000000000..a48b386c5 --- /dev/null +++ b/WSSiOS/Resource/Assets.xcassets/icon/icBookPlus.imageset/mdi_book-plus-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/WSSiOS/Resource/Constants/Strings/StringLiterals+Home.swift b/WSSiOS/Resource/Constants/Strings/StringLiterals+Home.swift index 95b76cfa8..186dbfa2b 100644 --- a/WSSiOS/Resource/Constants/Strings/StringLiterals+Home.swift +++ b/WSSiOS/Resource/Constants/Strings/StringLiterals+Home.swift @@ -15,11 +15,13 @@ extension StringLiterals { static let interest = "님의 관심글" static let notLoggedInInterest = "・ :*관심글*: ・" static let recommend = "이 웹소설은 어때요? (´ヮ`)ノ📚" + static let detailSearchBanner = "뭐 읽을지 고민될 때" } enum SubTitle { static let interest = "관심 등록한 작품의 최신 글이에요" static let recommend = "선호 장르를 기반으로 추천해드려요" + static let detailSearchBanner = "장르, 연재상태, 별점, 키워드로 작품 찾기" } enum Login { diff --git a/WSSiOS/Resource/Constants/Strings/StringLiterals+Novel.swift b/WSSiOS/Resource/Constants/Strings/StringLiterals+Novel.swift index d7b89bf6c..204ed4f39 100644 --- a/WSSiOS/Resource/Constants/Strings/StringLiterals+Novel.swift +++ b/WSSiOS/Resource/Constants/Strings/StringLiterals+Novel.swift @@ -100,7 +100,7 @@ extension StringLiterals { } enum Date { - static let addDate = "날짜 추가" + static let addDate = "본 날짜 추가" static let complete = "완료" static let removeDate = "날짜 삭제" static let startDate = "시작 날짜" diff --git a/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift b/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift index 2f8e0cc9a..852bfb100 100644 --- a/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift +++ b/WSSiOS/Resource/Constants/Strings/StringLiterals+Search.swift @@ -51,5 +51,7 @@ extension StringLiterals { static let placeHolder = "키워드를 검색하세요" static let empty = "해당하는 작품이 없어요\n검색의 범위를 더 넓혀보세요" + + static let applyOption = "장르, 연재상태, 별점, 키워드 적용" } } diff --git a/WSSiOS/Resource/Extensions/UIViewController+.swift b/WSSiOS/Resource/Extensions/UIViewController+.swift index dca49f361..3e1c22303 100644 --- a/WSSiOS/Resource/Extensions/UIViewController+.swift +++ b/WSSiOS/Resource/Extensions/UIViewController+.swift @@ -56,6 +56,10 @@ extension UIViewController { self.navigationItem.title = title self.navigationItem.leftBarButtonItem = left != nil ? UIBarButtonItem(customView: left!) : nil self.navigationItem.rightBarButtonItem = right != nil ? UIBarButtonItem(customView: right!) : nil + if #available(iOS 26.0, *) { + self.navigationItem.leftBarButtonItem?.hidesSharedBackground = true + self.navigationItem.rightBarButtonItem?.hidesSharedBackground = true + } setNavigationBarVisibleBeforeScroll(isVisible: isVisibleBeforeScroll) } @@ -69,7 +73,7 @@ extension UIViewController { ] $0.shadowColor = .clear } - + let whiteAppearance = UINavigationBarAppearance().then { $0.configureWithOpaqueBackground() $0.backgroundColor = .white @@ -80,7 +84,7 @@ extension UIViewController { ] $0.shadowColor = .clear } - + navigationItem.standardAppearance = whiteAppearance navigationItem.scrollEdgeAppearance = isVisible ? whiteAppearance : clearAppearance } @@ -306,17 +310,32 @@ extension UIViewController { self.navigationController?.pushViewController(viewController, animated: true) } - func presentToDetailSearchViewController(selectedKeywordList: [KeywordData], - previousViewInfo: PreviousViewType, - selectedFilteredQuery: SearchFilterQuery) { + func pushToDetailSearchViewController() { let detailSearchViewController = DetailSearchViewController( viewModel: DetailSearchViewModel( keywordRepository: DefaultKeywordRepository( - keywordService: DefaultKeywordService()), - selectedKeywordList: selectedKeywordList, - previousViewInfo: previousViewInfo, - selectedFilteredQuery: selectedFilteredQuery)) - self.presentModalViewController(detailSearchViewController) + keywordService: DefaultKeywordService() + ) + ) + ) + detailSearchViewController.navigationController?.isNavigationBarHidden = true + detailSearchViewController.hidesBottomBarWhenPushed = true + self.navigationController?.pushViewController(detailSearchViewController, + animated: true) + } + + func pushToDetailSearchResultViewController(option: SearchFilterQuery) { + let detailSearchResultViewController = DetailSearchResultViewController( + viewModel: DetailSearchResultViewModel( + searchRepository: DefaultSearchRepository( + searchService: DefaultSearchService()), + option: option + ) + ) + detailSearchResultViewController.navigationController?.isNavigationBarHidden = true + detailSearchResultViewController.hidesBottomBarWhenPushed = true + self.navigationController?.pushViewController(detailSearchResultViewController, + animated: true) } func presentInduceLoginViewController() { @@ -363,8 +382,8 @@ extension UIViewController { func pushToChangeUserInfoViewController() { let viewController = MyPageChangeUserInfoViewController( - userRepository: DefaultUserInfoRepository( - userService: DefaultUserService() + userRepository: DefaultUserInfoRepository( + userService: DefaultUserService() ) ) viewController.hidesBottomBarWhenPushed = true @@ -373,7 +392,7 @@ extension UIViewController { func pushToLibraryViewController(userId: Int, pageIndex: Int = 0) { let viewController = UserLibraryViewController(userId: userId) - + viewController.setPageIndex(target: pageIndex) viewController.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(viewController, animated: true) diff --git a/WSSiOS/Source/Data/Base/AttractivePoint.swift b/WSSiOS/Source/Data/Base/AttractivePoint.swift index 2a798b447..c779abccc 100644 --- a/WSSiOS/Source/Data/Base/AttractivePoint.swift +++ b/WSSiOS/Source/Data/Base/AttractivePoint.swift @@ -10,6 +10,7 @@ import UIKit enum AttractivePoint: String, CaseIterable, Codable { case worldview = "worldview" case material = "material" + case writingSkill = "writingskill" case character = "character" case relationship = "relationship" case vibe = "vibe" @@ -18,6 +19,7 @@ enum AttractivePoint: String, CaseIterable, Codable { switch self { case .worldview: "세계관" case .material: "소재" + case .writingSkill: "필력" case .character: "캐릭터" case .relationship: "관계" case .vibe: "분위기" @@ -28,6 +30,7 @@ enum AttractivePoint: String, CaseIterable, Codable { switch self { case .worldview: .icAttractiveWorldview case .material: .icAttractiveMaterial + case .writingSkill: .icAttractiveWritingSkill case .character: .icAttractiveCharacter case .relationship: .icAttractiveRelationship case .vibe: .icAttractiveVibe diff --git a/WSSiOS/Source/Data/Base/NovelGenre.swift b/WSSiOS/Source/Data/Base/NovelGenre.swift index bc0da3893..20c8ba0e1 100644 --- a/WSSiOS/Source/Data/Base/NovelGenre.swift +++ b/WSSiOS/Source/Data/Base/NovelGenre.swift @@ -8,33 +8,6 @@ import UIKit enum NovelGenre: String, CaseIterable { - case romance, romanceFantasy, fantasy, modernFantasy, wuxia, BL, lightNovel, mystery, drama - - var toKorean: String { - switch self { - case .romanceFantasy: - return "로판" - case .romance: - return "로맨스" - case .fantasy: - return "판타지" - case .modernFantasy: - return "현판" - case .drama: - return "드라마" - case .lightNovel: - return "라노벨" - case .wuxia: - return "무협" - case .mystery: - return "미스터리" - case .BL: - return "BL" - } - } -} - -enum NewNovelGenre: String, CaseIterable { case all = "all" case fantasy = "fantasy" case modernFantasy = "modernFantasy" @@ -77,7 +50,7 @@ enum NewNovelGenre: String, CaseIterable { } } - static func withKoreanRawValue(from genre: String) -> NewNovelGenre { + static func withKoreanRawValue(from genre: String) -> NovelGenre { switch genre { case "전체": return .all @@ -236,9 +209,11 @@ enum NewNovelGenre: String, CaseIterable { } } -extension NewNovelGenre { - static let onboardingGenres: [NewNovelGenre] = [.romance, .romanceFantasy, .bl, .fantasy, .modernFantasy, .wuxia, .lightNovel, .drama, .mystery] - static let feedMaleGenres: [NewNovelGenre] = [.all, .fantasy, .modernFantasy, .wuxia, .drama, .mystery, .lightNovel, .romance, .romanceFantasy, .bl, .etc] - static let feedFemaleGenres: [NewNovelGenre] = [.all, .romance, .romanceFantasy, .bl, .fantasy, .modernFantasy, .wuxia, .drama, .mystery, .lightNovel, .etc] - static let feedFilterGenres: [NewNovelGenre] = [.fantasy, .modernFantasy, .romance, .romanceFantasy, .wuxia, .mystery, .drama, .lightNovel, .bl, .etc] +extension NovelGenre { + static let onboardingGenres: [NovelGenre] = [.romance, .romanceFantasy, .bl, .fantasy, .modernFantasy, .wuxia, .lightNovel, .drama, .mystery] + static let feedMaleGenres: [NovelGenre] = [.all, .fantasy, .modernFantasy, .wuxia, .drama, .mystery, .lightNovel, .romance, .romanceFantasy, .bl, .etc] + static let feedFemaleGenres: [NovelGenre] = [.all, .romance, .romanceFantasy, .bl, .fantasy, .modernFantasy, .wuxia, .drama, .mystery, .lightNovel, .etc] + static let feedFilterGenres: [NovelGenre] = [.fantasy, .modernFantasy, .romance, .romanceFantasy, .wuxia, .mystery, .drama, .lightNovel, .bl, .etc] + static let detailSearchGenres: [NovelGenre] = [.fantasy, .modernFantasy, .romance, .romanceFantasy, .wuxia, .mystery, .drama, .lightNovel, .bl] + static let myPageEditGenres: [NovelGenre] = [.romance, .romanceFantasy, .fantasy, .modernFantasy, .wuxia, .bl, .lightNovel, .mystery, .drama] } diff --git a/WSSiOS/Source/Data/Base/CompletedStatus.swift b/WSSiOS/Source/Data/Base/PublicationStatus.swift similarity index 65% rename from WSSiOS/Source/Data/Base/CompletedStatus.swift rename to WSSiOS/Source/Data/Base/PublicationStatus.swift index 49d0ac063..77f021181 100644 --- a/WSSiOS/Source/Data/Base/CompletedStatus.swift +++ b/WSSiOS/Source/Data/Base/PublicationStatus.swift @@ -7,25 +7,25 @@ import Foundation -enum CompletedStatus: String, CaseIterable { +enum PublicationStatus: String, CaseIterable { + case onGoing case completed - case notCompleted var description: String { switch self { + case .onGoing: return "연재중" case .completed: return "완결작" - case .notCompleted: return "연재중" } } var isCompleted: Bool { switch self { + case .onGoing: return false case .completed: return true - case .notCompleted: return false } } init(isCompleted: Bool) { - self = isCompleted ? .completed : .notCompleted + self = isCompleted ? .completed : .onGoing } } diff --git a/WSSiOS/Source/Data/Entity/Feed/FeedEntity.swift b/WSSiOS/Source/Data/Entity/Feed/FeedEntity.swift index 87d10db69..a1a54da38 100644 --- a/WSSiOS/Source/Data/Entity/Feed/FeedEntity.swift +++ b/WSSiOS/Source/Data/Entity/Feed/FeedEntity.swift @@ -74,7 +74,7 @@ extension FeedResponse { let title = self.title, let rating = self.novelRating, let genreRaw = self.novelGenre, - let genre = NewNovelGenre(rawValue: genreRaw), + let genre = NovelGenre(rawValue: genreRaw), let description = self.novelDescription, let thumbnailPath = self.novelThumbnailImage, let thumbnailURL = KingFisherRxHelper.makeImageURLString(path: thumbnailPath) diff --git a/WSSiOS/Source/Data/Entity/Feed/FeedFilter.swift b/WSSiOS/Source/Data/Entity/Feed/FeedFilter.swift index b09dbbaea..2ac1ffbc1 100644 --- a/WSSiOS/Source/Data/Entity/Feed/FeedFilter.swift +++ b/WSSiOS/Source/Data/Entity/Feed/FeedFilter.swift @@ -33,6 +33,6 @@ enum FeedVisibilityOption: CaseIterable { } struct FeedFilterOption: Equatable { - var genres: [NewNovelGenre] = NewNovelGenre.feedFilterGenres + var genres: [NovelGenre] = NovelGenre.feedFilterGenres var visibilityOptions: [FeedVisibilityOption] = [.public, .private] } diff --git a/WSSiOS/Source/Data/Entity/Feed/TotalFeedListEntity.swift b/WSSiOS/Source/Data/Entity/Feed/TotalFeedListEntity.swift index 77b6cd0cc..6c6abb74f 100644 --- a/WSSiOS/Source/Data/Entity/Feed/TotalFeedListEntity.swift +++ b/WSSiOS/Source/Data/Entity/Feed/TotalFeedListEntity.swift @@ -91,7 +91,7 @@ extension TotalFeedResponse { let thumbnailImageURL = URL(string: self.thumbnailUrl ?? "") let hasImage = self.thumbnailUrl != nil && self.imageCount > 0 - let genre = NewNovelGenre(rawValue: self.genreName ?? "") + let genre = NovelGenre(rawValue: self.genreName ?? "") let novelGenreColor = genre?.linkColor ?? .genreColorR let novelGenreImage = genre?.linkImage ?? .icGenreLinkR diff --git a/WSSiOS/Source/Data/Entity/SearchFilterOption.swift b/WSSiOS/Source/Data/Entity/SearchFilterOption.swift new file mode 100644 index 000000000..edce8ddb6 --- /dev/null +++ b/WSSiOS/Source/Data/Entity/SearchFilterOption.swift @@ -0,0 +1,16 @@ +// +// SearchFilterOption.swift +// WSSiOS +// +// Created by Seoyeon Choi on 4/30/26. +// + +import Foundation + +struct SearchFilterQuery { + let keywords: [KeywordData] + let genres: [NovelGenre] + let isCompleted: Bool? + let lowerNovelRating: Float + let upperNovelRating: Float +} diff --git a/WSSiOS/Source/Data/Entity/UserFeedListEntity.swift b/WSSiOS/Source/Data/Entity/UserFeedListEntity.swift index ce52b6306..eec8f5854 100644 --- a/WSSiOS/Source/Data/Entity/UserFeedListEntity.swift +++ b/WSSiOS/Source/Data/Entity/UserFeedListEntity.swift @@ -55,14 +55,14 @@ extension UserFeedResponse { } let hasImage = self.thumbnailUrl != nil && self.imageCount > 0 - let genre = NewNovelGenre(rawValue: self.genre ?? "") + let genre = NovelGenre(rawValue: self.genre ?? "") let novelGenreColor = genre?.linkColor ?? .genreColorR let novelGenreImage = genre?.linkImage ?? .icGenreLinkR let thumbnailImageURL = KingFisherRxHelper.makeImageURLString(path: self.thumbnailUrl ?? "") return UserFeedEntity(feedId: self.feedId, feedContent: self.feedContent, - createdDate: self.formattedDate(), + createdDate: self.createdDate, isSpoiler: self.isSpoiler, isModified: self.isModified, isLiked: self.isLiked, @@ -78,21 +78,6 @@ extension UserFeedResponse { hasImage: hasImage, imageCount: self.imageCount) } - - private func formattedDate() -> String { - let inputDateFormatter = DateFormatter() - inputDateFormatter.dateFormat = "yyyy-MM-dd" - - guard let date = inputDateFormatter.date(from: self.createdDate) else { - return "" - } - - let outputDateFormatter = DateFormatter() - outputDateFormatter.locale = Locale(identifier: "ko_KR") - outputDateFormatter.dateFormat = "M월 d일" - - return outputDateFormatter.string(from: date) - } } struct UserFeedListItem { diff --git a/WSSiOS/Source/Data/Repository/OnboardingRepository.swift b/WSSiOS/Source/Data/Repository/OnboardingRepository.swift index 7be0a0f94..086eb7187 100644 --- a/WSSiOS/Source/Data/Repository/OnboardingRepository.swift +++ b/WSSiOS/Source/Data/Repository/OnboardingRepository.swift @@ -11,7 +11,7 @@ import RxSwift protocol OnboardingRepository { func getNicknameisValid(_ nickname: String) -> Single - func postUserProfile(nickname: String, gender: OnboardingGender, birth: Int, genrePreferences: [NewNovelGenre]) -> Single + func postUserProfile(nickname: String, gender: OnboardingGender, birth: Int, genrePreferences: [NovelGenre]) -> Single } struct TestOnboardingRepository: OnboardingRepository { @@ -19,7 +19,7 @@ struct TestOnboardingRepository: OnboardingRepository { return Single.just(OnboardingResponse(isValid: false)) } - func postUserProfile(nickname: String, gender: OnboardingGender, birth: Int, genrePreferences: [NewNovelGenre]) -> Single { + func postUserProfile(nickname: String, gender: OnboardingGender, birth: Int, genrePreferences: [NovelGenre]) -> Single { return Single.just(()) } } @@ -35,7 +35,7 @@ struct DefaultOnboardingRepository: OnboardingRepository { return onboardingService.getNicknameisValid(nickname) } - func postUserProfile(nickname: String, gender: OnboardingGender, birth: Int, genrePreferences: [NewNovelGenre]) -> Single { + func postUserProfile(nickname: String, gender: OnboardingGender, birth: Int, genrePreferences: [NovelGenre]) -> Single { let userInfoResult = UserInfoRequest( nickname: nickname, gender: gender.rawValue, diff --git a/WSSiOS/Source/Data/Repository/SearchRepository.swift b/WSSiOS/Source/Data/Repository/SearchRepository.swift index ae0016aa2..170d18c31 100644 --- a/WSSiOS/Source/Data/Repository/SearchRepository.swift +++ b/WSSiOS/Source/Data/Repository/SearchRepository.swift @@ -14,7 +14,8 @@ protocol SearchRepository { func getSearchNovels(query: String, page: Int) -> Observable func getDetailSearchNovels(genres: [String], isCompleted: Bool?, - novelRating: Float?, + lowerNovelRating: Float, + upperNovelRating: Float, keywordIds: [Int], page: Int) -> Observable } @@ -39,62 +40,16 @@ struct DefaultSearchRepository: SearchRepository { func getDetailSearchNovels(genres: [String], isCompleted: Bool?, - novelRating: Float?, + lowerNovelRating: Float, + upperNovelRating: Float, keywordIds: [Int], page: Int) -> Observable { return searchService.searchDetailNovels(genres: genres, isCompleted: isCompleted, - novelRating: novelRating, + lowerNovelRating: lowerNovelRating, + upperNovelRating: upperNovelRating, keywordIds: keywordIds, page: page, size: searchSize).asObservable() } } - -struct TestSearchRepository: SearchRepository { - func getSosoPickNovels() -> Observable { - return Observable.just(SosoPickNovels(sosoPicks: [ - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "구리구리스"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "구리구리뱅"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "상수리 나무 아래"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "하수리 나무 위"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "하수리수리마수리"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "딱대"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "토크쇼"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "배고파"), - SosoPickNovel(novelId: 1, novelImage: "imgTest2", novelTitle: "닭가슴살꼬치먹고싶다힝구힝구힝")])) - } - - func getSearchNovels(query: String, page: Int) -> Observable { - return Observable.just(NormalSearchNovels(resultCount: 1003, isLoadable: true, novels: [ - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리리구리", novelAuthor: "구리스구리스최서연최서연구리구리구리구리구리구리구리구리구리구리구리구리", interestCount: 13, novelRating: 2.34, novelRatingCount: 221), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/70/54/70/705470563de4ad028c5323fe2ac16628.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/70/54/70/705470563de4ad028c5323fe2ac16628.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/70/54/70/705470563de4ad028c5323fe2ac16628.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21)])) - } - - func getDetailSearchNovels(genres: [String], isCompleted: Bool?, novelRating: Float?, keywordIds: [Int], page: Int) -> Observable { - return Observable.just(DetailSearchNovels(resultCount: 1116, - isLoadable: true, - novels: [SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리구리리구리", novelAuthor: "구리스구리스최서연최서연구리구리구리구리구리구리구리구리구리구리구리구리", interestCount: 13, novelRating: 2.34, novelRatingCount: 221), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/70/54/70/705470563de4ad028c5323fe2ac16628.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/70/54/70/705470563de4ad028c5323fe2ac16628.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/70/54/70/705470563de4ad028c5323fe2ac16628.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/4a/63/a3/4a63a3eda53a5d685c8593869947d06c.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21), - SearchNovel(novelId: 2, novelImage: "https://i.pinimg.com/564x/f7/8f/e1/f78fe156e361a321b5d1334e5f21f031.jpg", novelTitle: "구리구리구리구리구리구리", novelAuthor: "구리스구리스최서연최서연", interestCount: 123, novelRating: 2.34, novelRatingCount: 21)])) - } -} diff --git a/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesOtherTableViewCell.swift b/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesOtherTableViewCell.swift index cceb09359..e89496c32 100644 --- a/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesOtherTableViewCell.swift +++ b/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesOtherTableViewCell.swift @@ -68,7 +68,7 @@ final class UserGenrePreferencesOtherTableViewCell: UITableViewCell { func bindData(data: UserGenrePreferencesEntity) { genreImageView.kfSetImage(url: data.genreImageURL) - let koreanGenre = NewNovelGenre(rawValue: data.genreName)?.withKorean + let koreanGenre = NovelGenre(rawValue: data.genreName)?.withKorean genreLabel.applyWSSFont(.title3, with: koreanGenre) countLabel.applyWSSFont(.body5, with: String(data.genreCount) + "편") } diff --git a/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesTopView.swift b/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesTopView.swift index 3f0839036..47cc80175 100644 --- a/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesTopView.swift +++ b/WSSiOS/Source/Presentation/Base/UserPreferences/UserGenrePreferencesTopView.swift @@ -70,7 +70,7 @@ final class UserGenrePreferencesTopView: UIView { func bindData(data: UserGenrePreferencesEntity) { topGenreImageView.kfSetImage(url: data.genreImageURL) - let koreanGenre = NewNovelGenre(rawValue: data.genreName)?.withKorean + let koreanGenre = NovelGenre(rawValue: data.genreName)?.withKorean topGenreTitleLabel.applyWSSFont(.title3, with: koreanGenre) topGenreCountLabel.applyWSSFont(.body5, with: String(data.genreCount) + "편") } diff --git a/WSSiOS/Source/Presentation/Base/WSSRangeSlider.swift b/WSSiOS/Source/Presentation/Base/WSSRangeSlider.swift new file mode 100644 index 000000000..25085b4fb --- /dev/null +++ b/WSSiOS/Source/Presentation/Base/WSSRangeSlider.swift @@ -0,0 +1,200 @@ +// +// WSSRangeSlider.swift +// WSSiOS +// +// Created by Seoyeon Choi on 4/29/26. +// + +import UIKit + +final class WSSRangeSlider: UIControl { + + //MARK: - Properties + + var minimumValue: CGFloat = 0.0 { didSet { updateLayerFrames() } } + var maximumValue: CGFloat = 5.0 { didSet { updateLayerFrames() } } + + private(set) var lowerValue: CGFloat = 0.0 { didSet { updateLayerFrames() } } + private(set) var upperValue: CGFloat = 5.0 { didSet { updateLayerFrames() } } + + var step: CGFloat = 0.5 + + private let thumbSize: CGFloat = 16 + private let trackHeight: CGFloat = 4 + + private var isDraggingLower = false + private var isDraggingUpper = false + + private let tickSize = CGSize(width: 1, height: 2) + + //MARK: - UI Components + + private let trackLayer = CALayer() + private var tickLayers: [CALayer] = [] + private let rangeLayer = CALayer() + private let lowerThumbView = UIView() + private let upperThumbView = UIView() + + //MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: frame) + + setUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: thumbSize) + } + + override func layoutSubviews() { + super.layoutSubviews() + updateLayerFrames() + } + + //MARK: - UI + + private func setUI() { + trackLayer.do { + $0.backgroundColor = UIColor.wssPrimary50.cgColor + $0.cornerRadius = trackHeight / 2 + } + layer.addSublayer(trackLayer) + + let tickCount = Int((maximumValue - minimumValue) / step) + 1 + for _ in 0.. CGFloat { + let trackWidth = bounds.width - thumbSize + return thumbSize / 2 + trackWidth * (value - minimumValue) / (maximumValue - minimumValue) + } + + private func valueForPosition(_ position: CGFloat) -> CGFloat { + let trackWidth = bounds.width - thumbSize + let ratio = (position - thumbSize / 2) / trackWidth + return minimumValue + (maximumValue - minimumValue) * max(0, min(1, ratio)) + } + + private func snapToStep(_ value: CGFloat) -> CGFloat { + (value / step).rounded() * step + } + + //MARK: - Touch Handling + + override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + let location = touch.location(in: self) + + let lowerDistance = abs(location.x - lowerThumbView.center.x) + let upperDistance = abs(location.x - upperThumbView.center.x) + let threshold = thumbSize * 1.5 + + if lowerValue == upperValue && lowerDistance < threshold { + if location.x >= lowerThumbView.center.x { + isDraggingUpper = true + } else { + isDraggingLower = true + } + } else if lowerDistance < upperDistance && lowerDistance < threshold { + isDraggingLower = true + } else if upperDistance < threshold { + isDraggingUpper = true + } + + return isDraggingLower || isDraggingUpper + } + + override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + let location = touch.location(in: self) + let rawValue = valueForPosition(location.x) + let snappedValue = snapToStep(rawValue) + + if isDraggingLower { + lowerValue = min(snappedValue, upperValue) + } else if isDraggingUpper { + upperValue = max(snappedValue, lowerValue) + } + + sendActions(for: .valueChanged) + return true + } + + override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + isDraggingLower = false + isDraggingUpper = false + } + + //MARK: - Custom Method + + func setValues(lower: CGFloat, upper: CGFloat) { + lowerValue = max(minimumValue, min(lower, maximumValue)) + upperValue = max(minimumValue, min(upper, maximumValue)) + } +} diff --git a/WSSiOS/Source/Presentation/Base/WSSTabBarItem.swift b/WSSiOS/Source/Presentation/Base/WSSTabBarItem.swift index e8c189978..76e7548ca 100644 --- a/WSSiOS/Source/Presentation/Base/WSSTabBarItem.swift +++ b/WSSiOS/Source/Presentation/Base/WSSTabBarItem.swift @@ -10,7 +10,7 @@ import UIKit enum WSSTabBarItem: Int, CaseIterable { case home = 0 - case search, feed, library, myPage + case feed, library, myPage var normalItemImage: UIImage { switch self { @@ -18,10 +18,6 @@ enum WSSTabBarItem: Int, CaseIterable { return .icNavigateHome .withRenderingMode(.alwaysOriginal) .withTintColor(.wssGray200) - case .search: - return .icNavigateSearch - .withRenderingMode(.alwaysOriginal) - .withTintColor(.wssGray200) case .feed: return .icNavigateFeed .withRenderingMode(.alwaysOriginal) @@ -43,10 +39,6 @@ enum WSSTabBarItem: Int, CaseIterable { return .icNavigateHomeSelected .withRenderingMode(.alwaysOriginal) .withTintColor(.wssBlack) - case .search: - return .icNavigateSearchSelected - .withRenderingMode(.alwaysOriginal) - .withTintColor(.wssBlack) case .feed: return .icNavigateFeedSelected .withRenderingMode(.alwaysOriginal) @@ -66,8 +58,6 @@ enum WSSTabBarItem: Int, CaseIterable { switch self { case .home: return StringLiterals.Tabbar.Title.home - case .search: - return StringLiterals.Tabbar.Title.search case .feed: return StringLiterals.Tabbar.Title.feed case .library: @@ -91,9 +81,6 @@ enum WSSTabBarItem: Int, CaseIterable { notificationService: DefaultNotificationService()) )) - case .search: - return SearchViewController(viewModel: SearchViewModel(searchRepository: DefaultSearchRepository(searchService: DefaultSearchService()))) - case .feed: return FeedViewController() diff --git a/WSSiOS/Source/Presentation/Feed/Filter/FeedFilterViewController/FeedFilterViewController.swift b/WSSiOS/Source/Presentation/Feed/Filter/FeedFilterViewController/FeedFilterViewController.swift index 92b954a28..157d58655 100644 --- a/WSSiOS/Source/Presentation/Feed/Filter/FeedFilterViewController/FeedFilterViewController.swift +++ b/WSSiOS/Source/Presentation/Feed/Filter/FeedFilterViewController/FeedFilterViewController.swift @@ -20,7 +20,7 @@ final class FeedFilterViewController: UIViewController { private let disposeBag = DisposeBag() private let initialFilterOption: FeedFilterOption let filterOption = PublishSubject() - private let genreOptions = BehaviorRelay<[NewNovelGenre]>(value: NewNovelGenre.feedFilterGenres) + private let genreOptions = BehaviorRelay<[NovelGenre]>(value: NovelGenre.feedFilterGenres) private let visibilityOptions = BehaviorRelay<[FeedVisibilityOption]>(value: FeedVisibilityOption.allCases) //MARK: - Components @@ -110,7 +110,7 @@ final class FeedFilterViewController: UIViewController { .subscribe(with: self, onNext: { owner, _ in let selectedIndexPaths = owner.rootView.genreView.genreCollectionView.indexPathsForSelectedItems ?? [] let selectedGenres = selectedIndexPaths.map { indexPath in - NewNovelGenre.feedFilterGenres[indexPath.row] + NovelGenre.feedFilterGenres[indexPath.row] } owner.genreOptions.accept(selectedGenres) @@ -119,7 +119,7 @@ final class FeedFilterViewController: UIViewController { } private func bindOutput() { - Observable<[NewNovelGenre]>.just(NewNovelGenre.feedFilterGenres) + Observable<[NovelGenre]>.just(NovelGenre.feedFilterGenres) .bind(to: rootView.genreView.genreCollectionView.rx.items( cellIdentifier: FeedFilterGenreCollectionViewCell.cellIdentifier, cellType: FeedFilterGenreCollectionViewCell .self)) { item, element, cell in @@ -178,7 +178,7 @@ extension FeedFilterViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { var text: String? - let novelGenreList = NewNovelGenre.feedFilterGenres.map { $0.withKorean } + let novelGenreList = NovelGenre.feedFilterGenres.map { $0.withKorean } text = novelGenreList[indexPath.item] guard let unwrappedText = text else { diff --git a/WSSiOS/Source/Presentation/FeedEdit/FeedEditView/FeedEditView.swift b/WSSiOS/Source/Presentation/FeedEdit/FeedEditView/FeedEditView.swift index 121d32936..00edae607 100644 --- a/WSSiOS/Source/Presentation/FeedEdit/FeedEditView/FeedEditView.swift +++ b/WSSiOS/Source/Presentation/FeedEdit/FeedEditView/FeedEditView.swift @@ -129,7 +129,7 @@ final class FeedEditView: UIView { func showAddImages(hasImage: Bool) { if hasImage { - stackView.insertArrangedSubview(feedEditAddImageView, at: 2) + stackView.insertArrangedSubview(feedEditAddImageView, at: 1) stackView.setCustomSpacing(14, after: feedEditContentView) } else { feedEditAddImageView.removeFromSuperview() diff --git a/WSSiOS/Source/Presentation/FeedEdit/FeedEditViewCell/FeedCategoryCollectionViewCell.swift b/WSSiOS/Source/Presentation/FeedEdit/FeedEditViewCell/FeedCategoryCollectionViewCell.swift index a83b8f9fd..23893d7c4 100644 --- a/WSSiOS/Source/Presentation/FeedEdit/FeedEditViewCell/FeedCategoryCollectionViewCell.swift +++ b/WSSiOS/Source/Presentation/FeedEdit/FeedEditViewCell/FeedCategoryCollectionViewCell.swift @@ -52,7 +52,7 @@ final class FeedCategoryCollectionViewCell: UICollectionViewCell { //MARK: - Data - func bindData(category: NewNovelGenre) { + func bindData(category: NovelGenre) { self.keywordLink.setText(category.withKorean) } } diff --git a/WSSiOS/Source/Presentation/Home/Home/HomeView/HomeAssistantView/HomeInduceDetailSearchView.swift b/WSSiOS/Source/Presentation/Home/Home/HomeView/HomeAssistantView/HomeInduceDetailSearchView.swift new file mode 100644 index 000000000..54625d9f9 --- /dev/null +++ b/WSSiOS/Source/Presentation/Home/Home/HomeView/HomeAssistantView/HomeInduceDetailSearchView.swift @@ -0,0 +1,116 @@ +// +// HomeInduceDetailSearchView.swift +// WSSiOS +// +// Created by Seoyeon Choi on 4/29/26. +// + +import UIKit + +import SnapKit +import Then + +final class HomeInduceDetailSearchView: UIView { + + //MARK: - UI Components + + private let imageView = UIImageView() + + private let labelStackView = UIStackView() + private let titleStackView = UIStackView() + private let titleLabel = UILabel() + private let titleNavigateImageView = UIImageView() + private let subTitleLabel = UILabel() + + //MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: frame) + + setUI() + setHierachy() + setLayout() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setUI() { + self.do { + $0.layer.cornerRadius = 14 + $0.clipsToBounds = true + $0.backgroundColor = .wssPrimary20 + } + + imageView.do { + $0.image = .imgHomeDetailSearch + $0.contentMode = .scaleAspectFit + } + + labelStackView.do { + $0.axis = .vertical + $0.spacing = 4 + } + + titleStackView.do { + $0.axis = .horizontal + $0.spacing = 6 + $0.alignment = .firstBaseline + } + + titleLabel.do { + $0.applyWSSFont(.title1, with: StringLiterals.Home.Title.detailSearchBanner) + $0.textColor = .wssBlack + + $0.setContentHuggingPriority(.required, for: .horizontal) + } + + titleNavigateImageView.do { + $0.image = .icNavigateRight + .withRenderingMode(.alwaysOriginal) + .withTintColor(.wssBlack) + + $0.contentMode = .left + $0.clipsToBounds = false + + $0.setContentHuggingPriority(.defaultLow, for: .horizontal) + $0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + + subTitleLabel.do { + $0.applyWSSFont(.body4, with: StringLiterals.Home.SubTitle.detailSearchBanner) + $0.textColor = .wssGray200 + } + } + + private func setHierachy() { + self.addSubviews(imageView, + labelStackView) + labelStackView.addArrangedSubviews(titleStackView, + subTitleLabel) + titleStackView.addArrangedSubviews (titleLabel, + titleNavigateImageView) + } + + private func setLayout() { + self.snp.makeConstraints { + $0.height.equalTo(104) + } + + imageView.snp.makeConstraints { + $0.trailing.equalToSuperview() + $0.bottom.equalToSuperview().offset(14) + } + + labelStackView.snp.makeConstraints { + $0.top.equalToSuperview().inset(24) + $0.leading.equalToSuperview().inset(20) + } + } + + //MARK: - Custom Method + +} + diff --git a/WSSiOS/Source/Presentation/Home/Home/HomeView/HomeView.swift b/WSSiOS/Source/Presentation/Home/Home/HomeView/HomeView.swift index 0db8f6540..597c87fa1 100644 --- a/WSSiOS/Source/Presentation/Home/Home/HomeView/HomeView.swift +++ b/WSSiOS/Source/Presentation/Home/Home/HomeView/HomeView.swift @@ -18,9 +18,9 @@ final class HomeView: UIView { private let contentView = UIView() let headerView = HomeHeaderView() let searchBarView = SearchBarView() + let induceDetailSearchView = HomeInduceDetailSearchView() let todayPopularView = HomeTodayPopularView() let realtimePopularView = HomeRealtimePopularView() - let interestView = HomeInterestView() let tasteRecommendView = HomeTasteRecommendView() let loadingView = WSSLoadingView() @@ -56,9 +56,9 @@ final class HomeView: UIView { loadingView) self.scrollView.addSubview(contentView) contentView.addSubviews(searchBarView, + induceDetailSearchView, todayPopularView, realtimePopularView, - interestView, tasteRecommendView) } @@ -92,8 +92,13 @@ final class HomeView: UIView { $0.height.equalTo(42) } + induceDetailSearchView.snp.makeConstraints { + $0.top.equalTo(searchBarView.snp.bottom).offset(20) + $0.leading.trailing.equalToSuperview().inset(20) + } + todayPopularView.snp.makeConstraints { - $0.top.equalTo(searchBarView.snp.bottom).offset(24) + $0.top.equalTo(induceDetailSearchView.snp.bottom).offset(24) $0.horizontalEdges.equalToSuperview() } @@ -102,13 +107,8 @@ final class HomeView: UIView { $0.horizontalEdges.equalToSuperview() } - interestView.snp.makeConstraints { - $0.top.equalTo(realtimePopularView.snp.bottom).offset(40) - $0.horizontalEdges.equalToSuperview() - } - tasteRecommendView.snp.makeConstraints { - $0.top.equalTo(interestView.snp.bottom).offset(40) + $0.top.equalTo(realtimePopularView.snp.bottom).offset(40) $0.horizontalEdges.bottom.equalToSuperview() } } diff --git a/WSSiOS/Source/Presentation/Home/Home/HomeViewController/HomeViewController.swift b/WSSiOS/Source/Presentation/Home/Home/HomeViewController/HomeViewController.swift index 4f8af0406..b45b2d752 100644 --- a/WSSiOS/Source/Presentation/Home/Home/HomeViewController/HomeViewController.swift +++ b/WSSiOS/Source/Presentation/Home/Home/HomeViewController/HomeViewController.swift @@ -81,11 +81,7 @@ final class HomeViewController: UIViewController { rootView.realtimePopularView.realtimePopularCollectionView.register( HomeRealtimePopularCollectionViewCell.self, forCellWithReuseIdentifier: HomeRealtimePopularCollectionViewCell.cellIdentifier) - - rootView.interestView.interestCollectionView.register( - HomeInterestCollectionViewCell.self, - forCellWithReuseIdentifier: HomeInterestCollectionViewCell.cellIdentifier) - + rootView.tasteRecommendView.tasteRecommendCollectionView.register( HomeTasteRecommendCollectionViewCell.self, forCellWithReuseIdentifier: HomeTasteRecommendCollectionViewCell.cellIdentifier) @@ -101,13 +97,12 @@ final class HomeViewController: UIViewController { viewWillAppearEvent: viewWillAppearEvent.asObservable(), viewDidLoadEvent: viewDidLoadEvent.asObservable(), todayPopularCellSelected: rootView.todayPopularView.todayPopularCollectionView.rx.itemSelected, - interestCellSelected: rootView.interestView.interestCollectionView.rx.itemSelected, tasteRecommendCellSelected: rootView.tasteRecommendView.tasteRecommendCollectionView.rx.itemSelected, tasteRecommendCollectionViewContentSize: rootView.tasteRecommendView.tasteRecommendCollectionView.rx.observe(CGSize.self, "contentSize"), announcementButtonDidTap: rootView.headerView.announcementButton.rx.tap, - registerInterestNovelButtonTapped: rootView.interestView.unregisterView.registerButton.rx.tap, setPreferredGenresButtonTapped: rootView.tasteRecommendView.unregisterView.registerButton.rx.tap, - searchBarViewDidTap: rootView.searchBarView.rx.tapGesture().when(.recognized).asObservable() + searchBarViewDidTap: rootView.searchBarView.rx.tapGesture().when(.recognized).asObservable(), + induceDetailSearchViewDidTap: rootView.induceDetailSearchView.rx.tapGesture().when(.recognized) ) let output = viewModel.transform(from: input, disposeBag: disposeBag) @@ -147,25 +142,6 @@ final class HomeViewController: UIViewController { }) .disposed(by: disposeBag) - // 관심글 - output.interestList - .bind(to: rootView.interestView.interestCollectionView.rx.items( - cellIdentifier: HomeInterestCollectionViewCell.cellIdentifier, - cellType: HomeInterestCollectionViewCell.self)) { row, element, cell in - cell.bindData(data: element) - } - .disposed(by: disposeBag) - - output.updateInterestView - .observe(on: MainScheduler.instance) - .subscribe(with: self, onNext: { owner, data in - let isLogined = data.0 - let message = data.1 - let nickname = UserDefaults.standard.string(forKey: StringLiterals.UserDefault.userNickname) - owner.rootView.interestView.updateView(isLogined, message, nickname) - }) - .disposed(by: disposeBag) - output.pushToNormalSearchViewController .bind(with: self, onNext: { owner, _ in owner.pushToNormalSearchViewController() @@ -220,6 +196,12 @@ final class HomeViewController: UIViewController { }) .disposed(by: disposeBag) + output.pushToDetailSearchViewController + .bind(with: self, onNext: { owner, _ in + owner.pushToDetailSearchViewController() + }) + .disposed(by: disposeBag) + output.showLoadingView .observe(on: MainScheduler.instance) .bind(with: self, onNext: { owner, isShow in diff --git a/WSSiOS/Source/Presentation/Home/Home/HomeViewModel/HomeViewModel.swift b/WSSiOS/Source/Presentation/Home/Home/HomeViewModel/HomeViewModel.swift index 79defa150..024b00c04 100644 --- a/WSSiOS/Source/Presentation/Home/Home/HomeViewModel/HomeViewModel.swift +++ b/WSSiOS/Source/Presentation/Home/Home/HomeViewModel/HomeViewModel.swift @@ -30,11 +30,6 @@ final class HomeViewModel: ViewModelType { private let realtimePopularList = PublishSubject<[RealtimePopularFeed]>() private let realtimePopularDataRelay = BehaviorRelay<[[RealtimePopularFeed]]>(value: []) - // 관심글 - private let interestList = BehaviorRelay<[InterestFeed]>(value: []) - private let updateInterestView = PublishRelay<(Bool, InterestMessage)>() - private var interestFeedMessage = BehaviorRelay(value: .none) - // 취향추천 private let tasteRecommendList = BehaviorRelay<[TasteRecommendNovel]>(value: []) private let updateTasteRecommendView = PublishRelay<(Bool, Bool)>() @@ -42,6 +37,7 @@ final class HomeViewModel: ViewModelType { private let pushToNovelDetailViewController = PublishRelay() private let pushToAnnouncementViewController = PublishRelay() + private let pushToDetailSearchViewController = PublishRelay() let showInduceLoginModalView = PublishRelay() private let showLoadingView = PublishRelay() @@ -55,13 +51,12 @@ final class HomeViewModel: ViewModelType { let viewWillAppearEvent: Observable let viewDidLoadEvent: Observable let todayPopularCellSelected: ControlEvent - let interestCellSelected: ControlEvent let tasteRecommendCellSelected: ControlEvent let tasteRecommendCollectionViewContentSize: Observable let announcementButtonDidTap: ControlEvent - let registerInterestNovelButtonTapped: ControlEvent let setPreferredGenresButtonTapped: ControlEvent let searchBarViewDidTap: Observable + let induceDetailSearchViewDidTap: Observable } //MARK: - Outputs @@ -74,9 +69,6 @@ final class HomeViewModel: ViewModelType { var realtimePopularList: Observable<[RealtimePopularFeed]> var realtimePopularData: Observable<[[RealtimePopularFeed]]> - var interestList: Observable<[InterestFeed]> - let updateInterestView: Observable<(Bool, InterestMessage)> - var tasteRecommendList: Observable<[TasteRecommendNovel]> let tasteRecommendCollectionViewHeight: Driver let updateTasteRecommendView: Observable<(Bool, Bool)> @@ -84,6 +76,7 @@ final class HomeViewModel: ViewModelType { let pushToNovelDetailViewController: Observable let pushToAnnouncementViewController: Observable + let pushToDetailSearchViewController: Observable let showInduceLoginModalView: Observable let showLoadingView: Observable let showUpdateVersionAlertView: Observable @@ -138,16 +131,9 @@ extension HomeViewModel { let message = InterestMessage(rawValue: interestFeeds.message) if owner.isLogined { - owner.interestList.accept(interestFeeds.recommendFeeds) - owner.updateInterestView.accept((true, message ?? .none)) - owner.interestFeedMessage.accept(message ?? .none) - owner.tasteRecommendList.accept(tasteRecommendNovels.tasteNovels) owner.updateTasteRecommendView.accept((true, tasteRecommendNovels.tasteNovels.isEmpty)) } else { - owner.updateInterestView.accept((false, message ?? .none)) - owner.interestFeedMessage.accept(.none) - owner.updateTasteRecommendView.accept((false, true)) } @@ -179,8 +165,6 @@ extension HomeViewModel { UserDefaults.standard.setValue(data.userId, forKey: StringLiterals.UserDefault.userId) UserDefaults.standard.setValue(data.nickname, forKey: StringLiterals.UserDefault.userNickname) UserDefaults.standard.setValue(data.gender, forKey: StringLiterals.UserDefault.userGender) - owner.updateInterestView.accept((self.isLogined, self.interestFeedMessage.value)) - owner.getTermSetting(disposeBag: disposeBag) }) .disposed(by: disposeBag) @@ -191,6 +175,16 @@ extension HomeViewModel { }) .disposed(by: disposeBag) + input.induceDetailSearchViewDidTap + .subscribe(with: self, onNext: { owner, _ in + if owner.isLogined { + owner.pushToDetailSearchViewController.accept(()) + } else { + owner.showInduceLoginModalView.accept(()) + } + }) + .disposed(by: disposeBag) + input.todayPopularCellSelected .subscribe(with: self, onNext: { owner, indexPath in AmplitudeManager.shared.track(AmplitudeEvent.Home.homeTodayRanking) @@ -202,15 +196,7 @@ extension HomeViewModel { } }) .disposed(by: disposeBag) - - input.interestCellSelected - .subscribe(with: self, onNext: { owner, indexPath in - AmplitudeManager.shared.track(AmplitudeEvent.Home.homeLoveFeedlist) - let novelId = owner.interestList.value[indexPath.row].novelId - owner.pushToNovelDetailViewController.accept(novelId) - }) - .disposed(by: disposeBag) - + input.tasteRecommendCellSelected .subscribe(with: self, onNext: { owner, indexPath in AmplitudeManager.shared.track(AmplitudeEvent.Home.homePreferNovellist) @@ -233,17 +219,6 @@ extension HomeViewModel { }) .disposed(by: disposeBag) - input.registerInterestNovelButtonTapped - .subscribe(with: self, onNext: { owner, _ in - AmplitudeManager.shared.track(AmplitudeEvent.Home.homeToLoveButton) - if owner.isLogined { - owner.pushToNormalSearchViewController.accept(()) - } else { - owner.showInduceLoginModalView.accept(()) - } - }) - .disposed(by: disposeBag) - input.setPreferredGenresButtonTapped .subscribe(with: self, onNext: { owner, _ in AmplitudeManager.shared.track(AmplitudeEvent.Home.homeToPreferButton) @@ -259,14 +234,13 @@ extension HomeViewModel { todayPopularList: todayPopularList.asObservable(), realtimePopularList: realtimePopularList.asObservable(), realtimePopularData: realtimePopularDataRelay.asObservable(), - interestList: interestList.asObservable(), - updateInterestView: updateInterestView.asObservable(), tasteRecommendList: tasteRecommendList.asObservable(), tasteRecommendCollectionViewHeight: tasteRecommendCollectionViewHeight.asDriver(), updateTasteRecommendView: updateTasteRecommendView.asObservable(), pushToMyPageEditViewController: pushToMyPageViewController.asObservable(), pushToNovelDetailViewController: pushToNovelDetailViewController.asObservable(), pushToAnnouncementViewController: pushToAnnouncementViewController.asObservable(), + pushToDetailSearchViewController: pushToDetailSearchViewController.asObservable(), showInduceLoginModalView: showInduceLoginModalView.asObservable(), showLoadingView: showLoadingView.asObservable(), showUpdateVersionAlertView: showUpdateVersionAlertView.asObservable(), diff --git a/WSSiOS/Source/Presentation/Library/LibraryFilter/LibraryFilterView/LibraryFilterAssistantView/LibraryFilterAttractivePointOptionButton.swift b/WSSiOS/Source/Presentation/Library/LibraryFilter/LibraryFilterView/LibraryFilterAssistantView/LibraryFilterAttractivePointOptionButton.swift index 7824b38c2..1494f4f0c 100644 --- a/WSSiOS/Source/Presentation/Library/LibraryFilter/LibraryFilterView/LibraryFilterAssistantView/LibraryFilterAttractivePointOptionButton.swift +++ b/WSSiOS/Source/Presentation/Library/LibraryFilter/LibraryFilterView/LibraryFilterAssistantView/LibraryFilterAttractivePointOptionButton.swift @@ -49,13 +49,13 @@ final class LibraryFilterAttractivePointOptionButton: UIButton { statusLabel.do { $0.applyWSSFont(.body4, with: attractivePoint.koreanString) - $0.textColor = .wssGray300 + $0.textColor = .wssGray200 $0.isUserInteractionEnabled = false } statusImageView.do { $0.image = attractivePoint.image.withRenderingMode(.alwaysTemplate) - $0.tintColor = .wssGray100 + $0.tintColor = .wssGray80 $0.contentMode = .scaleAspectFit $0.isUserInteractionEnabled = false } @@ -86,7 +86,7 @@ final class LibraryFilterAttractivePointOptionButton: UIButton { func updateButton(selectedOptions: [AttractivePoint]) { let isSelected = selectedOptions.contains(where: {$0 == self.attractivePoint}) - statusImageView.tintColor = isSelected ? .wssPrimary100 : .wssGray100 - statusLabel.textColor = isSelected ? .wssPrimary100 : .wssGray300 + statusImageView.tintColor = isSelected ? .wssPrimary100 : .wssGray80 + statusLabel.textColor = isSelected ? .wssPrimary100 : .wssGray200 } } diff --git a/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryAssistantView/MyLibraryNavigationView.swift b/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryAssistantView/MyLibraryNavigationView.swift index 597536dc0..07ea4f1a4 100644 --- a/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryAssistantView/MyLibraryNavigationView.swift +++ b/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryAssistantView/MyLibraryNavigationView.swift @@ -13,8 +13,9 @@ import Then final class MyLibraryNavigationView: UIView { //MARK: - Components - + private let navigationTitle = UILabel() + let libraryAddButton = UIButton() // MARK: - Life Cycle @@ -39,21 +40,31 @@ final class MyLibraryNavigationView: UIView { $0.applyWSSFont(.headline1, with: StringLiterals.Navigation.Title.library) $0.textColor = .wssBlack } + + libraryAddButton.do { + $0.setImage(.icBookPlus, for: .normal) + } } - + private func setHierarchy() { - addSubviews(navigationTitle) + addSubviews(navigationTitle, libraryAddButton) } - + private func setLayout() { self.snp.makeConstraints { $0.height.equalTo(56) } - + navigationTitle.snp.makeConstraints { $0.centerY.equalToSuperview() $0.leading.equalToSuperview().inset(20) } + + libraryAddButton.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().inset(20) + $0.size.equalTo(24) + } } } diff --git a/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryView.swift b/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryView.swift index 3dfcade7f..faeed3808 100644 --- a/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryView.swift +++ b/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryView/MyLibraryView.swift @@ -14,7 +14,7 @@ final class MyLibraryView: UIView { //MARK: - Components - private let navigationView = MyLibraryNavigationView() + let navigationView = MyLibraryNavigationView() let headerView = MyLibraryHeaderView() let libraryCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) let libraryTableView = UITableView(frame: .zero, style: .plain) diff --git a/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryViewController/MyLibraryViewController.swift b/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryViewController/MyLibraryViewController.swift index fdbe99505..ecdc6bb6a 100644 --- a/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryViewController/MyLibraryViewController.swift +++ b/WSSiOS/Source/Presentation/Library/MyLibrary/MyLibraryViewController/MyLibraryViewController.swift @@ -231,6 +231,13 @@ final class MyLibraryViewController: UIViewController { HapticManager.shared.generateSelectionFeedback() }) .disposed(by: disposeBag) + + rootView.navigationView.libraryAddButton.rx.tap + .asDriver() + .drive(with: self, onNext: { owner, _ in + owner.pushToNormalSearchViewController() + }) + .disposed(by: disposeBag) } } diff --git a/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewController/NovelDetailViewController.swift b/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewController/NovelDetailViewController.swift index 3ee095cb5..2b8c8f5ce 100644 --- a/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewController/NovelDetailViewController.swift +++ b/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewController/NovelDetailViewController.swift @@ -61,7 +61,7 @@ final class NovelDetailViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - + registerCell() delegate() bindViewModel() @@ -74,13 +74,14 @@ final class NovelDetailViewController: UIViewController { viewWillAppearEvent.accept(()) setNavigationBar() swipeBackGesture() - self.hidesBottomBarWhenPushed = true } //MARK: - UI private func setNavigationBar() { self.setWSSNavigationBar(title: navigationTitle, left: rootView.backButton, right: rootView.headerDropDownButton, isVisibleBeforeScroll: false) + setContentScrollView(rootView.scrollView, for: .top) + self.hidesBottomBarWhenPushed = true } //MARK: - Bind diff --git a/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewModel/NovelDetailViewModel.swift b/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewModel/NovelDetailViewModel.swift index 5f4d0b1d3..fb02f7b14 100644 --- a/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewModel/NovelDetailViewModel.swift +++ b/WSSiOS/Source/Presentation/NovelDetail/NovelDetailViewModel/NovelDetailViewModel.swift @@ -36,7 +36,7 @@ final class NovelDetailViewModel: ViewModelType { private let showLargeNovelCoverImage = BehaviorRelay(value: false) private let isUserNovelInterested = BehaviorRelay(value: false) private let readStatus = BehaviorRelay(value: nil) - private let novelGenre = BehaviorRelay<[NewNovelGenre]>(value: []) + private let novelGenre = BehaviorRelay<[NovelGenre]>(value: []) private let pushToAuthorSearchResultViewController = PublishRelay() // Tab @@ -570,7 +570,7 @@ final class NovelDetailViewModel: ViewModelType { owner.novelGenre.accept(data.novelGenre.split{ $0 == "/"} .map{ String($0) } - .map { NewNovelGenre.withKoreanRawValue(from: $0) }) + .map { NovelGenre.withKoreanRawValue(from: $0) }) }, onFailure: { owner, error in owner.showNetworkErrorView.accept(true) print("Error: \(error)") diff --git a/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewRatingView.swift b/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewRatingView.swift index 15bfb3b2c..25c9fb7bd 100644 --- a/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewRatingView.swift +++ b/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewRatingView.swift @@ -43,6 +43,7 @@ final class NovelReviewRatingView: UIView { $0.applyWSSFont(.title3, with: StringLiterals.NovelReview.rating) $0.textColor = .wssBlack } + starImageStackView.do { $0.axis = .horizontal $0.spacing = 10 @@ -84,6 +85,8 @@ final class NovelReviewRatingView: UIView { let starImageView = UIImageView().then { $0.isUserInteractionEnabled = true $0.image = .icLargeStarEmpty + .withRenderingMode(.alwaysOriginal) + .withTintColor(.wssGray80) $0.contentMode = .scaleAspectFill $0.clipsToBounds = true } @@ -103,6 +106,8 @@ final class NovelReviewRatingView: UIView { imageView.image = .icLargeStarHalf default: imageView.image = .icLargeStarEmpty + .withRenderingMode(.alwaysOriginal) + .withTintColor(.wssGray80) } } } diff --git a/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewStatusView.swift b/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewStatusView.swift index 4fc4832bb..a1afbb704 100644 --- a/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewStatusView.swift +++ b/WSSiOS/Source/Presentation/NovelReview/NovelReviewView/NovelReviewAssistantView/NovelReviewStatusView.swift @@ -14,7 +14,8 @@ final class NovelReviewStatusView: UIView { //MARK: - Components - let statusCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) + let statusCollectionView = UICollectionView(frame: .zero, + collectionViewLayout: UICollectionViewLayout()) let dateLabel = UILabel() private let dateFormatter = DateFormatter() diff --git a/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelKeywordSelectSearchResultCollectionViewCell.swift b/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelKeywordSelectSearchResultCollectionViewCell.swift index 0b43fdf35..7f2ba113a 100644 --- a/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelKeywordSelectSearchResultCollectionViewCell.swift +++ b/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelKeywordSelectSearchResultCollectionViewCell.swift @@ -46,7 +46,6 @@ final class NovelKeywordSelectSearchResultCollectionViewCell: UICollectionViewCe private func setLayout() { keywordLink.snp.makeConstraints { $0.edges.equalToSuperview() - $0.height.equalTo(35) } } diff --git a/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelReviewStatusCollectionViewCell.swift b/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelReviewStatusCollectionViewCell.swift index 850ed4d0f..8cbf67f49 100644 --- a/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelReviewStatusCollectionViewCell.swift +++ b/WSSiOS/Source/Presentation/NovelReview/NovelReviewViewCell/NovelReviewStatusCollectionViewCell.swift @@ -44,7 +44,7 @@ final class NovelReviewStatusCollectionViewCell: UICollectionViewCell { private func setUI() { statusImageView.do { $0.contentMode = .scaleAspectFit - $0.tintColor = .wssGray200 + $0.tintColor = .wssGray80 } titleLabel.do { @@ -77,7 +77,8 @@ final class NovelReviewStatusCollectionViewCell: UICollectionViewCell { switch status { case .watching: statusImageView.do { - $0.image = UIImage(resource: .icNovelReviewWatching).withRenderingMode(.alwaysTemplate) + $0.image = UIImage(resource: .icNovelReviewWatching) + .withRenderingMode(.alwaysTemplate) } titleLabel.do { @@ -85,7 +86,8 @@ final class NovelReviewStatusCollectionViewCell: UICollectionViewCell { } case .watched: statusImageView.do { - $0.image = UIImage(resource: .icNovelReviewWatched).withRenderingMode(.alwaysTemplate) + $0.image = UIImage(resource: .icNovelReviewWatched) + .withRenderingMode(.alwaysTemplate) } titleLabel.do { @@ -93,7 +95,8 @@ final class NovelReviewStatusCollectionViewCell: UICollectionViewCell { } case .quit: statusImageView.do { - $0.image = UIImage(resource: .icNovelReviewQuit).withRenderingMode(.alwaysTemplate) + $0.image = UIImage(resource: .icNovelReviewQuit) + .withRenderingMode(.alwaysTemplate) } titleLabel.do { @@ -106,7 +109,7 @@ final class NovelReviewStatusCollectionViewCell: UICollectionViewCell { private func updateTintColor(isSelected: Bool) { statusImageView.do { - $0.tintColor = isSelected ? .wssPrimary100 : .wssGray200 + $0.tintColor = isSelected ? .wssPrimary100 : .wssGray80 } titleLabel.do { diff --git a/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenreButtonView.swift b/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenreButtonView.swift index 0d5633959..8bbcf599e 100644 --- a/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenreButtonView.swift +++ b/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenreButtonView.swift @@ -14,7 +14,7 @@ final class OnboardingGenreButtonView: UIView { //MARK: - Properties - let genre: NewNovelGenre + let genre: NovelGenre private let buttonPaddingSum: CGFloat = 126 private var buttonSize: CGFloat { @@ -34,7 +34,7 @@ final class OnboardingGenreButtonView: UIView { //MARK: - Life Cycle - init(genre: NewNovelGenre) { + init(genre: NovelGenre) { self.genre = genre super.init(frame: .zero) @@ -108,7 +108,7 @@ final class OnboardingGenreButtonView: UIView { // MARK: - Custom Method - func updateButton(selectedGenres: [NewNovelGenre]) { + func updateButton(selectedGenres: [NovelGenre]) { let isSelected = selectedGenres.contains(genre) genreButton.do { diff --git a/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenrePreferenceView.swift b/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenrePreferenceView.swift index 824aec94a..1f30d5359 100644 --- a/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenrePreferenceView.swift +++ b/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingView/OnboardingAssistantView/OnboardingGenrePreferenceView/OnboardingGenrePreferenceView.swift @@ -17,7 +17,7 @@ final class OnboardingGenrePreferenceView: UIView { private let titleLabel = UILabel() private let descriptionLabel = UILabel() - let genreButtons: [OnboardingGenreButtonView] = NewNovelGenre.onboardingGenres + let genreButtons: [OnboardingGenreButtonView] = NovelGenre.onboardingGenres .map { OnboardingGenreButtonView(genre: $0) } let totalGenreStackView = UIStackView() @@ -113,7 +113,7 @@ final class OnboardingGenrePreferenceView: UIView { // MARK: - Custom Method - func updateGenreButtons(selectedGenres: [NewNovelGenre]) { + func updateGenreButtons(selectedGenres: [NovelGenre]) { genreButtons.forEach { $0.updateButton(selectedGenres: selectedGenres) } diff --git a/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingViewModel/OnboardingViewModel.swift b/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingViewModel/OnboardingViewModel.swift index c19450ad3..344b495ff 100644 --- a/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingViewModel/OnboardingViewModel.swift +++ b/WSSiOS/Source/Presentation/Onboarding/Onboarding/OnboardingViewModel/OnboardingViewModel.swift @@ -34,7 +34,7 @@ final class OnboardingViewModel: ViewModelType { private let isBirthGenderNextButtonAvailable = BehaviorRelay(value: false) // GenrePreference - private let selectedGenres = BehaviorRelay<[NewNovelGenre]>(value: []) + private let selectedGenres = BehaviorRelay<[NovelGenre]>(value: []) private let isGenrePreferenceNextButtonAvailable = BehaviorRelay(value: false) // Total @@ -71,7 +71,7 @@ final class OnboardingViewModel: ViewModelType { let selectedBirth: Observable // GenrePreference - let genreButtonDidTap: Observable + let genreButtonDidTap: Observable // Total let viewDidLoadEvent: Observable @@ -97,7 +97,7 @@ final class OnboardingViewModel: ViewModelType { let isBirthGenderNextButtonEnabled: Driver // GenrePrefernece - let selectedGenres: Driver<[NewNovelGenre]> + let selectedGenres: Driver<[NovelGenre]> let isGenrePreferenceNextButtonEnabled: Driver // Total diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchCompletedStatusButton.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchCompletedStatusButton.swift index 098d1c9cd..4775c1351 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchCompletedStatusButton.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchCompletedStatusButton.swift @@ -14,7 +14,7 @@ final class DetailSearchCompletedStatusButton: UIButton { //MARK: - Properties - let status: CompletedStatus + let status: PublicationStatus //MARK: - Components @@ -22,7 +22,7 @@ final class DetailSearchCompletedStatusButton: UIButton { //MARK: - Life Cycle - init(status: CompletedStatus) { + init(status: PublicationStatus) { self.status = status super.init(frame: .zero) @@ -63,7 +63,7 @@ final class DetailSearchCompletedStatusButton: UIButton { // MARK: - Custom Method - func updateButton(selectedCompletedStatus: CompletedStatus?) { + func updateButton(selectedCompletedStatus: PublicationStatus?) { let isSelected = selectedCompletedStatus == status self.do { diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchHeaderView.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchHeaderView.swift index 503042cc9..1e3f2bd44 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchHeaderView.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchHeaderView.swift @@ -18,12 +18,18 @@ final class DetailSearchHeaderView: UIView { //MARK: - UI Components + let backButton = UIButton() + let infoLabel = UILabel() let newInfoImageView = UIImageView() let keywordLabel = UILabel() let newKeywordImageView = UIImageView() let underLineView = UIView() + let resetStackView = UIStackView() + private let resetImageView = UIImageView() + private let resetLabel = UILabel() + //MARK: - Life Cycle override init(frame: CGRect) { @@ -40,6 +46,15 @@ final class DetailSearchHeaderView: UIView { } private func setUI() { + backButton.do { + $0.setImage( + .icNavigateLeft + .withRenderingMode(.alwaysOriginal) + .withTintColor(.wssBlack), + for: .normal + ) + } + infoLabel.do { $0.applyWSSFont(.title1, with: StringLiterals.DetailSearch.info) $0.textColor = .wssPrimary100 @@ -58,19 +73,48 @@ final class DetailSearchHeaderView: UIView { $0.image = .icSearchNew.withTintColor(.wssPrimary100) $0.isHidden = true } + + resetStackView.do { + $0.axis = .horizontal + $0.spacing = 4 + } + + resetImageView.do { + $0.image = .icReload + .withRenderingMode(.alwaysOriginal) + .withTintColor(.wssGray300) + $0.contentMode = .scaleAspectFit + } + + resetLabel.do { + $0.applyWSSFont(.title2, with: StringLiterals.DetailSearch.reload) + $0.textColor = .wssGray300 + } } private func setHierarchy() { - self.addSubviews(infoLabel, + self.addSubviews(backButton, + infoLabel, newInfoImageView, keywordLabel, newKeywordImageView, - underLineView) + underLineView, + resetStackView) + + resetStackView.addArrangedSubviews(resetImageView, + resetLabel) } private func setLayout() { + backButton.snp.makeConstraints { + $0.size.equalTo(44) + $0.leading.equalToSuperview().inset(6) + $0.top.bottom.equalToSuperview() + } + infoLabel.snp.makeConstraints { - $0.top.leading.equalToSuperview() + $0.top.equalToSuperview().inset(7) + $0.leading.equalTo(backButton.snp.trailing).offset(14) } newInfoImageView.snp.makeConstraints { @@ -82,7 +126,6 @@ final class DetailSearchHeaderView: UIView { keywordLabel.snp.makeConstraints { $0.top.equalTo(infoLabel.snp.top) $0.leading.equalTo(infoLabel.snp.trailing).offset(29.5) - $0.trailing.equalToSuperview() } newKeywordImageView.snp.makeConstraints { @@ -95,7 +138,15 @@ final class DetailSearchHeaderView: UIView { $0.top.equalTo(keywordLabel.snp.bottom).offset(6) $0.horizontalEdges.equalTo(infoLabel.snp.horizontalEdges) $0.height.equalTo(2) - $0.bottom.equalToSuperview() + $0.bottom.equalToSuperview().inset(4) + } + + resetStackView.snp.makeConstraints { + $0.trailing.centerY.equalToSuperview() + + resetImageView.snp.makeConstraints { + $0.size.equalTo(14) + } } } @@ -121,14 +172,14 @@ final class DetailSearchHeaderView: UIView { $0.top.equalTo(self.infoLabel.snp.bottom).offset(6) $0.horizontalEdges.equalTo(self.infoLabel.snp.horizontalEdges) $0.height.equalTo(2) - $0.bottom.equalToSuperview() + $0.bottom.equalToSuperview().inset(4) } case .keyword: self.underLineView.snp.remakeConstraints { $0.top.equalTo(self.keywordLabel.snp.bottom).offset(6) $0.horizontalEdges.equalTo(self.keywordLabel.snp.horizontalEdges) $0.height.equalTo(2) - $0.bottom.equalToSuperview() + $0.bottom.equalToSuperview().inset(4) } } self.layoutIfNeeded() diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchInfoView.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchInfoView.swift index 2c6162805..5c0606efe 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchInfoView.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchInfoView.swift @@ -23,15 +23,16 @@ final class DetailSearchInfoView: UIView { private let statusTitleLabel = UILabel() private let statusStackView = UIStackView() - let completedStatusButtons = CompletedStatus.allCases.map { DetailSearchCompletedStatusButton(status: $0) } + let completedStatusButtons = PublicationStatus.allCases.map { DetailSearchCompletedStatusButton(status: $0) } /// 평점 private let ratingTitleLabel = UILabel() - - private let ratingTopStackView = UIStackView() - private let ratingBottomStackView = UIStackView() - - let novelRatingStatusButtons = NovelRatingStatus.allCases.map { WSSNovelRatingStatusButton(status: $0) } + let ratingSlider = WSSRangeSlider() + private let ratingValueLabel = UILabel() + private let ratingMinLabelBackgroundView = UIView() + private let ratingMinLabel = UILabel() + private let ratingMaxLabelBackgroundView = UIView() + private let ratingMaxLabel = UILabel() //MARK: - Life Cycle @@ -81,36 +82,50 @@ final class DetailSearchInfoView: UIView { $0.applyWSSFont(.title2, with: StringLiterals.DetailSearch.rating) $0.textColor = .wssBlack } - - ratingTopStackView.do { - $0.axis = .horizontal - $0.spacing = 11 - $0.distribution = .fillEqually + + ratingValueLabel.do { + $0.applyWSSFont(.title2, with: "0.0 ~ 5.0") + $0.textColor = .wssPrimary100 } - - ratingBottomStackView.do { - $0.axis = .horizontal - $0.spacing = 11 - $0.distribution = .fillEqually + + ratingSlider.do { + $0.minimumValue = 0.0 + $0.maximumValue = 5.0 + $0.step = 0.5 + $0.setValues(lower: 0.0, upper: 5.0) + } + + ratingMinLabel.do { + $0.applyWSSFont(.body2, with: "0.0") + $0.textColor = .wssPrimary100 + } + + [ratingMinLabelBackgroundView, + ratingMaxLabelBackgroundView].forEach { + $0.backgroundColor = .wssGray50 + $0.layer.cornerRadius = 8 + } + + ratingMaxLabel.do { + $0.applyWSSFont(.body2, with: "5.0") + $0.textColor = .wssPrimary100 } } private func setHierarchy() { completedStatusButtons.forEach { statusStackView.addArrangedSubview($0) } - - let topRowButtons = Array(novelRatingStatusButtons.prefix(2)) - let bottomRowButtons = Array(novelRatingStatusButtons.suffix(2)) - - topRowButtons.forEach { ratingTopStackView.addArrangedSubview($0) } - bottomRowButtons.forEach { ratingBottomStackView.addArrangedSubview($0) } + ratingMinLabelBackgroundView.addSubview(ratingMinLabel) + ratingMaxLabelBackgroundView.addSubview(ratingMaxLabel) self.addSubviews(genreTitleLabel, genreCollectionView, statusTitleLabel, statusStackView, ratingTitleLabel, - ratingTopStackView, - ratingBottomStackView) + ratingValueLabel, + ratingMinLabelBackgroundView, + ratingSlider, + ratingMaxLabelBackgroundView) } private func setLayout() { @@ -122,7 +137,7 @@ final class DetailSearchInfoView: UIView { genreCollectionView.snp.makeConstraints { $0.top.equalTo(genreTitleLabel.snp.bottom).offset(16) $0.leading.trailing.equalToSuperview().inset(20) - $0.height.equalTo(84) + $0.height.equalTo(88) } statusTitleLabel.snp.makeConstraints { @@ -140,35 +155,62 @@ final class DetailSearchInfoView: UIView { $0.top.equalTo(statusStackView.snp.bottom).offset(42) $0.leading.equalToSuperview().inset(20) } - - ratingTopStackView.snp.makeConstraints { + + ratingValueLabel.snp.makeConstraints { + $0.centerY.equalTo(ratingTitleLabel) + $0.trailing.equalToSuperview().inset(20) + } + + ratingMinLabelBackgroundView.snp.makeConstraints { $0.top.equalTo(ratingTitleLabel.snp.bottom).offset(16) - $0.leading.trailing.equalToSuperview().inset(20) - $0.height.equalTo(43) + $0.leading.equalToSuperview().inset(20) + $0.width.equalTo(50) + $0.height.equalTo(38) + + ratingMinLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } } - - ratingBottomStackView.snp.makeConstraints { - $0.top.equalTo(ratingTopStackView.snp.bottom).offset(10) - $0.leading.trailing.equalToSuperview().inset(20) - $0.height.equalTo(43) + + ratingSlider.snp.makeConstraints { + $0.centerY.equalTo(ratingMinLabel) + $0.leading.equalTo(ratingMinLabelBackgroundView.snp.trailing).offset(17) + $0.trailing.equalTo(ratingMaxLabelBackgroundView.snp.leading).offset(-17) + $0.height.equalTo(16) + } + + ratingMaxLabelBackgroundView.snp.makeConstraints { + $0.top.equalTo(ratingMinLabelBackgroundView.snp.top) + $0.trailing.equalToSuperview().inset(20) + $0.width.equalTo(50) + $0.height.equalTo(38) + + ratingMaxLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } } } - func updateCompletedKeyword(_ selectedCompletedStatus: CompletedStatus?) { + //MARK: - Custom Method + + func updateCompletedKeyword(_ selectedCompletedStatus: PublicationStatus?) { completedStatusButtons.forEach { $0.updateButton(selectedCompletedStatus: selectedCompletedStatus) } } - func updateNovelRatingKeyword(_ selectedNovelRatingStatus: NovelRatingStatus?) { - novelRatingStatusButtons.forEach { - $0.updateButton(selectedNovelRatingStatus: selectedNovelRatingStatus) - } - } - func resetAllStates() { genreCollectionView.indexPathsForSelectedItems?.forEach { indexPath in genreCollectionView.deselectItem(at: indexPath, animated: false) } } + + func updateRatingLabels(lower: CGFloat, upper: CGFloat) { + let lowerText = String(format: "%.1f", lower) + let upperText = String(format: "%.1f", upper) + ratingMinLabel.applyWSSFont(.body2, with: lowerText) + ratingMaxLabel.applyWSSFont(.body2, with: upperText) + ratingValueLabel.applyWSSFont(.body2, with: "\(lowerText) ~ \(upperText)") + ratingValueLabel.textColor = .wssPrimary100 + } } diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchResultHeaderView.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchResultHeaderView.swift index bfa28f2b8..dbb1aac17 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchResultHeaderView.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchAssistantView/DetailSearchResultHeaderView.swift @@ -36,21 +36,39 @@ final class DetailSearchResultHeaderView: UIView { backButton.do { $0.setImage(.icNavigateLeft.withRenderingMode(.alwaysOriginal).withTintColor(.wssBlack), for: .normal) } - + backgroundView.do { $0.backgroundColor = .wssGray50 $0.layer.cornerRadius = 14 - + headerLabel.do { - $0.applyWSSFont(.body4, with: "장르, 연재상태, 별점, 키워드 적용") + $0.applyWSSFont(.body4, with: "별점 적용") $0.textColor = .wssGray200 } - + controllerImageView.do { $0.image = .icController.withRenderingMode(.alwaysOriginal).withTintColor(.wssBlack) } } } + + func updateHeaderLabel(with option: SearchFilterQuery) { + var appliedFilters: [String] = [] + + if !option.genres.isEmpty { + appliedFilters.append(StringLiterals.DetailSearch.genre) + } + if option.isCompleted != nil { + appliedFilters.append(StringLiterals.DetailSearch.serialStatus) + } + appliedFilters.append(StringLiterals.DetailSearch.rating) + if !option.keywords.isEmpty { + appliedFilters.append(StringLiterals.DetailSearch.keyword) + } + + let text = appliedFilters.joined(separator: ", ") + " 적용" + headerLabel.applyWSSFont(.body4, with: text) + } private func setHierarchy() { backgroundView.addSubviews(headerLabel, diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchCell/DetailSearchInfoGenreCollectionViewCell.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchCell/DetailSearchInfoGenreCollectionViewCell.swift index de3de5daf..d37f83ed6 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchCell/DetailSearchInfoGenreCollectionViewCell.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchCell/DetailSearchInfoGenreCollectionViewCell.swift @@ -46,7 +46,6 @@ final class DetailSearchInfoGenreCollectionViewCell: UICollectionViewCell { private func setLayout() { genreKeywordView.snp.makeConstraints { $0.edges.equalToSuperview() - $0.height.equalTo(35) } } diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchView.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchView.swift index 1ac3f2e4f..810bc2dfd 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchView.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchView/DetailSearchView.swift @@ -14,17 +14,13 @@ final class DetailSearchView: UIView { //MARK: - UI Components - private let backgroundView = UIView() - let cancelModalButton = UIButton() - let detailSearchHeaderView = DetailSearchHeaderView() let detailSearchInfoView = DetailSearchInfoView() let detailSearchKeywordView = DetailSearchKeywordView() - let detailSearchBottomView = WSSSearchBottomActionView() - - // Home Indicator 배경 - private let backgroundBottomView = UIView() + let detailSearchButton = UIButton() + private let detailSearchButtonLabel = UILabel() + //MARK: - Life Cycle override init(frame: CGRect) { @@ -39,69 +35,59 @@ final class DetailSearchView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setUI() { - backgroundView.do { + self.do { $0.backgroundColor = .wssWhite - $0.layer.cornerRadius = 15 - $0.layer.maskedCorners = [.layerMinXMinYCorner, - .layerMaxXMinYCorner] } - cancelModalButton.do { - $0.setImage(.icCancelModal.withRenderingMode(.alwaysOriginal).withTintColor(.wssGray300), for: .normal) + detailSearchButton.do { + $0.backgroundColor = .wssPrimary100 + $0.layer.cornerRadius = 14 + $0.isEnabled = true } - backgroundBottomView.do { - $0.backgroundColor = .wssWhite + detailSearchButtonLabel.do { + $0.applyWSSFont(.title1, with: "작품 찾기") + $0.textColor = .wssWhite } } private func setHierarchy() { - backgroundView.addSubviews(cancelModalButton, - detailSearchHeaderView, - detailSearchInfoView, - detailSearchKeywordView, - detailSearchBottomView) - self.addSubviews(backgroundView, - backgroundBottomView) + self.addSubviews(detailSearchHeaderView, + detailSearchInfoView, + detailSearchKeywordView, + detailSearchButton) + detailSearchButton.addSubview(detailSearchButtonLabel) } private func setLayout() { - backgroundView.snp.makeConstraints { - $0.top.equalToSuperview().inset(82) - $0.leading.trailing.bottom.equalToSuperview() - - cancelModalButton.snp.makeConstraints { - $0.size.equalTo(25) - $0.top.trailing.equalToSuperview().inset(20) - } - - detailSearchHeaderView.snp.makeConstraints { - $0.top.leading.equalToSuperview().inset(34) - } - - detailSearchKeywordView.snp.makeConstraints { - $0.top.equalTo(detailSearchHeaderView.snp.bottom).offset(UIScreen.isSE ? 15 : 30) - $0.leading.trailing.equalToSuperview() - $0.bottom.equalTo(detailSearchBottomView.snp.top) - } - - detailSearchInfoView.snp.makeConstraints { - $0.top.equalTo(detailSearchHeaderView.snp.bottom).offset(UIScreen.isSE ? 15 : 30) - $0.leading.trailing.equalToSuperview() - $0.bottom.equalTo(detailSearchBottomView.snp.top) - } - - detailSearchBottomView.snp.makeConstraints { - $0.bottom.equalTo(safeAreaLayoutGuide.snp.bottom) - $0.leading.trailing.equalToSuperview() - } + detailSearchHeaderView.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide.snp.top) + $0.leading.equalToSuperview().inset(6) + $0.trailing.equalToSuperview().inset(16) + } + + detailSearchKeywordView.snp.makeConstraints { + $0.top.equalTo(detailSearchHeaderView.snp.bottom).offset(UIScreen.isSE ? 15 : 30) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(detailSearchButton.snp.top).offset(-10) + } + + detailSearchInfoView.snp.makeConstraints { + $0.top.equalTo(detailSearchHeaderView.snp.bottom).offset(UIScreen.isSE ? 15 : 30) + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(detailSearchButton.snp.top).offset(-10) + } + + detailSearchButton.snp.makeConstraints { + $0.bottom.equalTo(safeAreaLayoutGuide.snp.bottom).offset(-10) + $0.leading.trailing.equalToSuperview().inset(16) } - backgroundBottomView.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide.snp.bottomMargin) - $0.horizontalEdges.bottom.equalToSuperview() + detailSearchButtonLabel.snp.makeConstraints { + $0.verticalEdges.equalTo(detailSearchButton).inset(14) + $0.centerX.equalToSuperview() } } diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchResultViewController.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchResultViewController.swift index 62a9df8ba..2ed3910b3 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchResultViewController.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchResultViewController.swift @@ -41,11 +41,12 @@ final class DetailSearchResultViewController: UIViewController, UIScrollViewDele override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - setNavigationBar() + swipeBackGesture() - AmplitudeManager.shared.track(AmplitudeEvent.Search.seekResult) + + self.navigationController?.setNavigationBarHidden(true, + animated: true) } override func viewDidLoad() { @@ -56,14 +57,11 @@ final class DetailSearchResultViewController: UIViewController, UIScrollViewDele bindViewModel() bindAction() - + + rootView.headerView.updateHeaderLabel(with: viewModel.option) viewDidLoadEvent.accept(()) } - - private func setNavigationBar() { - self.navigationController?.isNavigationBarHidden = true - } - + private func registerCell() { rootView.novelView.resultNovelCollectionView.register(HomeTasteRecommendCollectionViewCell.self, forCellWithReuseIdentifier: HomeTasteRecommendCollectionViewCell.cellIdentifier) @@ -83,10 +81,9 @@ final class DetailSearchResultViewController: UIViewController, UIScrollViewDele backButtonDidTap: rootView.headerView.backButton.rx.tap, novelCollectionViewContentSize: rootView.novelView.resultNovelCollectionView.rx.observe(CGSize.self, "contentSize"), novelResultCellSelected: rootView.novelView.resultNovelCollectionView.rx.itemSelected, - searchHeaderViewDidTap: rootView.headerView.backgroundView.rx.tapGesture().when(.recognized).asObservable(), viewDidLoadEvent: self.viewDidLoadEvent.asObservable(), novelCollectionViewReachedBottom: observeReachedBottom(rootView.novelView.scrollView), - updateDetailSearchResultNotification: NotificationCenter.default.rx.notification(Notification.Name("PushToUpdateDetailSearchResult")) + searchBarViewDidTap: rootView.headerView.backgroundView.rx.tapGesture().when(.recognized) ) let output = viewModel.transform(from: input, disposeBag: disposeBag) @@ -122,14 +119,6 @@ final class DetailSearchResultViewController: UIViewController, UIScrollViewDele }) .disposed(by: disposeBag) - output.presentDetailSearchModal - .subscribe(with: self, onNext: { owner, data in - owner.presentToDetailSearchViewController(selectedKeywordList: data.keywords, - previousViewInfo: .resultSearchBar, - selectedFilteredQuery: data) - }) - .disposed(by: disposeBag) - output.showEmptyView .observe(on: MainScheduler.instance) .subscribe(with: self, onNext: { owner, show in diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchViewController.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchViewController.swift index a42734298..232d23ec7 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchViewController.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewController/DetailSearchViewController.swift @@ -43,6 +43,11 @@ final class DetailSearchViewController: UIViewController, UIScrollViewDelegate { self.view = rootView } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + swipeBackGesture() + } + override func viewDidLoad() { super.viewDidLoad() @@ -94,24 +99,22 @@ final class DetailSearchViewController: UIViewController, UIScrollViewDelegate { button.rx.tap.map { button.status } }) - let novelRatingStatusButtonDidTap = Observable.merge( - rootView.detailSearchInfoView.novelRatingStatusButtons - .map { button in - button.rx.tap.map { button.status } - }) - + let ratingSlider = rootView.detailSearchInfoView.ratingSlider + let ratingSliderValueChanged = ratingSlider.rx.controlEvent(.valueChanged) + .map { (ratingSlider.lowerValue, ratingSlider.upperValue) } + .asObservable() + let input = DetailSearchViewModel.Input( viewDidLoadEvent: viewDidLoadEvent.asObservable(), - closeButtonDidTap: rootView.cancelModalButton.rx.tap, + closeButtonDidTap: rootView.detailSearchHeaderView.backButton.rx.tap, infoTabDidTap: rootView.detailSearchHeaderView.infoLabel.rx.tapGesture().when(.recognized).asObservable(), keywordTabDidTap: rootView.detailSearchHeaderView.keywordLabel.rx.tapGesture().when(.recognized).asObservable(), - resetButtonDidTap: rootView.detailSearchBottomView.resetButton.rx.tap, - searchNovelButtonDidTap: rootView.detailSearchBottomView.searchButton.rx.tap, - updateDetailSearchResultData: NotificationCenter.default.rx.notification(Notification.Name("PushToUpateDetailSearchResult")).asObservable(), + resetViewDidTap: rootView.detailSearchHeaderView.resetStackView.rx.tapGesture().when(.recognized), + searchNovelButtonDidTap: rootView.detailSearchButton.rx.tap, genreColletionViewItemSelected: rootView.detailSearchInfoView.genreCollectionView.rx.itemSelected.asObservable(), genreColletionViewItemDeselected: rootView.detailSearchInfoView.genreCollectionView.rx.itemDeselected.asObservable(), - completedButtonDidTap: completedStatusButtonDidTap, - novelRatingButtonDidTap: novelRatingStatusButtonDidTap, + publicationStatusButtonDidTap: completedStatusButtonDidTap, + ratingSliderValueChanged: ratingSliderValueChanged, updatedEnteredText: rootView.detailSearchKeywordView.novelKeywordSelectSearchBarView.keywordTextField.rx.text.orEmpty.distinctUntilChanged().asObservable(), keywordTextFieldEditingDidBegin: rootView.detailSearchKeywordView.novelKeywordSelectSearchBarView.keywordTextField.rx.controlEvent(.editingDidBegin).asControlEvent(), keywordTextFieldEditingDidEnd: rootView.detailSearchKeywordView.novelKeywordSelectSearchBarView.keywordTextField.rx.controlEvent(.editingDidEnd).asControlEvent(), @@ -128,9 +131,9 @@ final class DetailSearchViewController: UIViewController, UIScrollViewDelegate { let output = viewModel.transform(from: input, disposeBag: disposeBag) // 전체 - output.dismissModalViewController + output.popViewController .bind(with: self, onNext: { owner, _ in - owner.dismissModalViewController() + owner.popToLastViewController() }) .disposed(by: disposeBag) @@ -152,6 +155,12 @@ final class DetailSearchViewController: UIViewController, UIScrollViewDelegate { }) .disposed(by: disposeBag) + output.pushToResultViewController + .subscribe(with: self, onNext: { owner, filterQuery in + owner.pushToDetailSearchResultViewController(option: filterQuery) + }) + .disposed(by: disposeBag) + // 정보 뷰 output.genreListData .bind(to: rootView.detailSearchInfoView.genreCollectionView.rx.items(cellIdentifier: DetailSearchInfoGenreCollectionViewCell.cellIdentifier,cellType: DetailSearchInfoGenreCollectionViewCell.self)) { item, element, cell in @@ -165,23 +174,24 @@ final class DetailSearchViewController: UIViewController, UIScrollViewDelegate { self.rootView.detailSearchInfoView.genreCollectionView.deselectItem(at: indexPath, animated: false) } - cell.bindData(genre: element.toKorean) + cell.bindData(genre: element.withKorean) } .disposed(by: disposeBag) - output.selectedCompletedStatus + output.selectedPublicationStatus .drive(with: self, onNext: { owner, selectedCompletedStatus in owner.rootView.detailSearchInfoView.updateCompletedKeyword(selectedCompletedStatus) }) .disposed(by: disposeBag) - output.selectedNovelRatingStatus - .drive(with: self, onNext: { owner, selectedNovelRatingStatus in - owner.rootView.detailSearchInfoView.updateNovelRatingKeyword(selectedNovelRatingStatus) + output.ratingRange + .drive(with: self, onNext: { owner, range in + owner.rootView.detailSearchInfoView.ratingSlider.setValues(lower: range.0, upper: range.1) + owner.rootView.detailSearchInfoView.updateRatingLabels(lower: range.0, upper: range.1) }) .disposed(by: disposeBag) - + output.resetSelectedInfoData .subscribe(with: self, onNext: { owner, _ in owner.rootView.detailSearchInfoView.resetAllStates() @@ -329,7 +339,7 @@ extension DetailSearchViewController: UICollectionViewDelegateFlowLayout { if collectionView == rootView.detailSearchInfoView.genreCollectionView { var text: String? - let novelGenreList = NovelGenre.allCases.map { $0.toKorean } + let novelGenreList = NovelGenre.detailSearchGenres.map { $0.withKorean } text = novelGenreList[indexPath.item] guard let unwrappedText = text else { @@ -337,7 +347,7 @@ extension DetailSearchViewController: UICollectionViewDelegateFlowLayout { } let width = (unwrappedText as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.Body2]).width + 26 - return CGSize(width: width, height: 35) + return CGSize(width: width, height: 37) } else if collectionView == rootView.detailSearchKeywordView.novelSelectedKeywordListView.selectedKeywordCollectionView{ var text: String? diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchResultViewModel.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchResultViewModel.swift index 7e10eac5f..1106bcd23 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchResultViewModel.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchResultViewModel.swift @@ -16,11 +16,8 @@ final class DetailSearchResultViewModel: ViewModelType { private let searchRepository: SearchRepository - // API 쿼리 - var keywords: [KeywordData] - var genres: [NovelGenre] - var isCompleted: Bool? - var novelRating: Float? + // 검색 필터 옵션 + var option: SearchFilterQuery // 무한 스크롤 private var currentPage: Int = 0 @@ -31,31 +28,25 @@ final class DetailSearchResultViewModel: ViewModelType { private let popViewController = PublishRelay() private let novelCollectionViewHeight = BehaviorRelay(value: 0) private let pushToNovelDetailViewController = PublishRelay() - private let presentDetailSearchModal = PublishRelay() private let showEmptyView = PublishRelay() private let filteredNovelsData = BehaviorRelay<[SearchNovel]>(value: []) private let resultCount = BehaviorRelay(value: 0) - private let updateDetailSearchResultNotification = PublishRelay() private let showLoadingView = PublishRelay() struct Input { let backButtonDidTap: ControlEvent let novelCollectionViewContentSize: Observable let novelResultCellSelected: ControlEvent - let searchHeaderViewDidTap: Observable - let viewDidLoadEvent: Observable let novelCollectionViewReachedBottom: Observable - let updateDetailSearchResultNotification: Observable + let searchBarViewDidTap: Observable } struct Output { let popViewController: Observable let novelCollectionViewHeight: Observable let pushToNovelDetailViewController: Observable - let presentDetailSearchModal: Observable - let filteredNovelsData: Observable<[SearchNovel]> let resultCount: Driver let showEmptyView: Observable @@ -63,15 +54,9 @@ final class DetailSearchResultViewModel: ViewModelType { } init(searchRepository: SearchRepository, - keywords: [KeywordData], - genres: [NovelGenre], - isCompleted: Bool?, - novelRating: Float?) { + option: SearchFilterQuery) { self.searchRepository = searchRepository - self.keywords = keywords - self.genres = genres - self.isCompleted = isCompleted - self.novelRating = novelRating + self.option = option } func transform(from input: Input, disposeBag: DisposeBag) -> Output { @@ -96,28 +81,17 @@ final class DetailSearchResultViewModel: ViewModelType { .bind(to: pushToNovelDetailViewController) .disposed(by: disposeBag) - input.searchHeaderViewDidTap - .subscribe(with: self, onNext: { owner, _ in - let filterQuery = SearchFilterQuery( - keywords: owner.keywords, - genres: owner.genres, - isCompleted: owner.isCompleted, - novelRating: owner.novelRating - ) - owner.presentDetailSearchModal.accept(filterQuery) - }) - .disposed(by: disposeBag) - input.viewDidLoadEvent .do(onNext: { self.showLoadingView.accept(true) }) .flatMapLatest { return self.getDetailSearchNovels( - genres: self.genres.map { $0.rawValue }, - isCompleted: self.isCompleted, - novelRating: self.novelRating, - keywordIds: self.keywords.map { $0.keywordId }, + genres: self.option.genres.map { $0.rawValue }, + isCompleted: self.option.isCompleted, + lowerNovelRating: self.option.lowerNovelRating, + upperNovelRating: self.option.upperNovelRating, + keywordIds: self.option.keywords.map { $0.keywordId }, page: 0 ) } @@ -146,10 +120,11 @@ final class DetailSearchResultViewModel: ViewModelType { }) .flatMapLatest { _ in self.getDetailSearchNovels( - genres: self.genres.map { $0.rawValue }, - isCompleted: self.isCompleted, - novelRating: self.novelRating, - keywordIds: self.keywords.map { $0.keywordId }, + genres: self.option.genres.map { $0.rawValue }, + isCompleted: self.option.isCompleted, + lowerNovelRating: self.option.lowerNovelRating, + upperNovelRating: self.option.upperNovelRating, + keywordIds: self.option.keywords.map { $0.keywordId }, page: self.currentPage + 1) .do(onNext: { _ in self.currentPage += 1 @@ -165,51 +140,15 @@ final class DetailSearchResultViewModel: ViewModelType { }) .disposed(by: disposeBag) - input.updateDetailSearchResultNotification - .do(onNext: { _ in - self.showLoadingView.accept(true) - }) - .subscribe(with: self, onNext: { owner, notification in - owner.updateDetailSearchResultNotification.accept(notification) - owner.filteredNovelsData.accept([]) - - if let userInfo = notification.userInfo { - let keywords = userInfo["keywords"] as? [KeywordData] - let genres = userInfo["genres"] as? [NovelGenre] - let isCompleted = userInfo["isCompleted"] as? Bool - let novelRating = userInfo["novelRating"] as? Float - - owner.keywords = keywords ?? [] - owner.genres = genres ?? [] - owner.isCompleted = isCompleted - owner.novelRating = novelRating - owner.currentPage = 0 - - owner.getDetailSearchNovels( - genres: owner.genres.map { $0.rawValue }, - isCompleted: owner.isCompleted, - novelRating: owner.novelRating, - keywordIds: owner.keywords.map { $0.keywordId }, - page: 0 - ) - .subscribe(onNext: { result in - owner.filteredNovelsData.accept(result.novels) - owner.resultCount.accept(result.resultCount) - owner.isLoadable = result.isLoadable - owner.showLoadingView.accept(false) - }, onError: { error in - print("Error fetching novels: \(error)") - owner.showLoadingView.accept(false) - }) - .disposed(by: disposeBag) - } + input.searchBarViewDidTap + .subscribe(with: self, onNext: { owner, _ in + owner.popViewController.accept(()) }) .disposed(by: disposeBag) return Output(popViewController: popViewController.asObservable(), novelCollectionViewHeight: novelCollectionViewHeight.asObservable(), pushToNovelDetailViewController: pushToNovelDetailViewController.asObservable(), - presentDetailSearchModal: presentDetailSearchModal.asObservable(), filteredNovelsData: filteredNovelsData.asObservable(), resultCount: resultCount.asDriver(), showEmptyView: showEmptyView.asObservable(), @@ -220,20 +159,15 @@ final class DetailSearchResultViewModel: ViewModelType { private func getDetailSearchNovels(genres: [String], isCompleted: Bool?, - novelRating: Float?, + lowerNovelRating: Float, + upperNovelRating: Float, keywordIds: [Int], page: Int) -> Observable { searchRepository.getDetailSearchNovels(genres: genres, isCompleted: isCompleted, - novelRating: novelRating, + lowerNovelRating: lowerNovelRating, + upperNovelRating: upperNovelRating, keywordIds: keywordIds, page: page) } } - -struct SearchFilterQuery { - let keywords: [KeywordData] - let genres: [NovelGenre] - let isCompleted: Bool? - let novelRating: Float? -} diff --git a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchViewModel.swift b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchViewModel.swift index 2f7ef6705..ccc259418 100644 --- a/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchViewModel.swift +++ b/WSSiOS/Source/Presentation/Search/DetailSearch/DetailSearchViewModel/DetailSearchViewModel.swift @@ -17,26 +17,24 @@ final class DetailSearchViewModel: ViewModelType { //MARK: - Properties private let keywordRepository: KeywordRepository - private let previousViewInfo: PreviousViewType - private let selectedFilteredQuery: SearchFilterQuery - + // 전체 private let dismissModalViewController = PublishRelay() let selectedTab = BehaviorRelay(value: DetailSearchTab.info) - private let pushToDetailSearchResultViewControllerNotificationName = Notification.Name("PushToDetailSearchResult") - private let pushToUpdateDetailSearchResultViewControllerNotificationName = Notification.Name("PushToUpdateDetailSearchResult") + let pushToResultViewController = PublishRelay() // 정보 private var selectedGenreList: [NovelGenre] = [] let selectedGenreListData = BehaviorRelay<[NovelGenre]>(value: []) private let genreListData = PublishRelay<[NovelGenre]>() - private var selectedCompletedStatus = BehaviorRelay(value: nil) - private var selectedNovelRatingStatus = BehaviorRelay(value: nil) + private var selectedCompletedStatus = BehaviorRelay(value: nil) + private let selectedRatingLower = BehaviorRelay(value: 0.0) + private let selectedRatingUpper = BehaviorRelay(value: 5.0) private let resetSelectedInfoData = PublishRelay() // 키워드 var keywordSearchResultList: [KeywordData] = [] - var selectedKeywordList: [KeywordData] + var selectedKeywordList: [KeywordData] = [] let keywordLimit: Int = 20 private let enteredText = BehaviorRelay(value: "") @@ -55,16 +53,14 @@ final class DetailSearchViewModel: ViewModelType { let closeButtonDidTap: ControlEvent let infoTabDidTap: Observable let keywordTabDidTap: Observable - let resetButtonDidTap: ControlEvent + let resetViewDidTap: Observable let searchNovelButtonDidTap: ControlEvent - let updateDetailSearchResultData: Observable // 정보 let genreColletionViewItemSelected: Observable let genreColletionViewItemDeselected: Observable - - let completedButtonDidTap: Observable - let novelRatingButtonDidTap: Observable + let publicationStatusButtonDidTap: Observable + let ratingSliderValueChanged: Observable<(CGFloat, CGFloat)> // 키워드 let updatedEnteredText: Observable @@ -83,15 +79,16 @@ final class DetailSearchViewModel: ViewModelType { struct Output { // 전체 - let dismissModalViewController: Observable + let popViewController: Observable let selectedTab: Driver let showInfoNewImageView: Observable let showKeywordNewImageView: Observable + let pushToResultViewController: Observable // 정보 let genreListData: Observable<[NovelGenre]> - let selectedCompletedStatus: Driver - let selectedNovelRatingStatus: Driver + let selectedPublicationStatus: Driver + let ratingRange: Driver<(CGFloat, CGFloat)> let resetSelectedInfoData: Observable // 키워드 @@ -108,25 +105,15 @@ final class DetailSearchViewModel: ViewModelType { //MARK: - init - init(keywordRepository: KeywordRepository, - selectedKeywordList: [KeywordData], - previousViewInfo: PreviousViewType, - selectedFilteredQuery: SearchFilterQuery) { + init(keywordRepository: KeywordRepository) { self.keywordRepository = keywordRepository - self.selectedKeywordList = selectedKeywordList - self.previousViewInfo = previousViewInfo - self.selectedFilteredQuery = selectedFilteredQuery } func transform(from input: Input, disposeBag: DisposeBag) -> Output { // 전체 input.viewDidLoadEvent .subscribe(with: self, onNext: { owner, _ in - owner.genreListData.accept(NovelGenre.allCases) - owner.selectedGenreListData.accept(owner.selectedFilteredQuery.genres) - owner.selectedKeywordListData.accept(owner.selectedFilteredQuery.keywords) - owner.selectedCompletedStatus.accept(owner.selectedFilteredQuery.isCompleted.map { CompletedStatus(isCompleted: $0) }) - owner.selectedNovelRatingStatus.accept(owner.selectedFilteredQuery.novelRating.map { NovelRatingStatus(toFloat: $0) }) + owner.genreListData.accept(NovelGenre.detailSearchGenres) }) .disposed(by: disposeBag) @@ -162,22 +149,25 @@ final class DetailSearchViewModel: ViewModelType { }) .disposed(by: disposeBag) - input.resetButtonDidTap + input.resetViewDidTap .subscribe(with: self, onNext: { owner, _ in - // 정보뷰 - owner.selectedGenreList = [] - owner.selectedGenreListData.accept(owner.selectedGenreList) - owner.resetSelectedInfoData.accept(()) - owner.selectedCompletedStatus.accept(nil) - owner.selectedNovelRatingStatus.accept(nil) - - // 키워드뷰 - owner.selectedKeywordList = [] - owner.selectedKeywordListData.accept(owner.selectedKeywordList) - owner.enteredText.accept("") - owner.keywordSearchResultListData.accept([]) - owner.showEmptyView.accept(false) - owner.showCategoryListView.accept(true) + if owner.selectedTab.value == .info { + // 정보뷰 + owner.selectedGenreList = [] + owner.selectedGenreListData.accept(owner.selectedGenreList) + owner.resetSelectedInfoData.accept(()) + owner.selectedCompletedStatus.accept(nil) + owner.selectedRatingLower.accept(0.0) + owner.selectedRatingUpper.accept(5.0) + } else { + // 키워드뷰 + owner.selectedKeywordList = [] + owner.selectedKeywordListData.accept(owner.selectedKeywordList) + owner.enteredText.accept("") + owner.keywordSearchResultListData.accept([]) + owner.showEmptyView.accept(false) + owner.showCategoryListView.accept(true) + } }) .disposed(by: disposeBag) @@ -187,33 +177,26 @@ final class DetailSearchViewModel: ViewModelType { let keywords = owner.selectedKeywordList let genres: [NovelGenre] = owner.selectedGenreListData.value let isCompleted = owner.selectedCompletedStatus.value?.isCompleted - let novelRating = owner.selectedNovelRatingStatus.value?.toFloat - - let userInfo: [AnyHashable: Any] = [ - "keywords": keywords, - "genres": genres, - "isCompleted": isCompleted as Any, - "novelRating": novelRating as Any - ] - - if owner.previousViewInfo == .search { - NotificationCenter.default.post(name: owner.pushToDetailSearchResultViewControllerNotificationName, - object: nil, - userInfo: userInfo) - owner.dismissModalViewController.accept(()) - } else { - NotificationCenter.default.post(name: owner.pushToUpdateDetailSearchResultViewControllerNotificationName, - object: nil, - userInfo: userInfo) - owner.dismissModalViewController.accept(()) - } + let lowernovelRating = Float(owner.selectedRatingLower.value) + let uppernovelRating = Float(owner.selectedRatingUpper.value) + + let filterQuery = SearchFilterQuery( + keywords: keywords, + genres: genres, + isCompleted: isCompleted, + lowerNovelRating: lowernovelRating, + upperNovelRating: uppernovelRating + ) + owner.pushToResultViewController.accept(filterQuery) }) .disposed(by: disposeBag) + // MARK: - 정보 + input.genreColletionViewItemSelected .subscribe(with: self, onNext: { owner, indexPath in owner.selectedGenreList = owner.selectedGenreListData.value - owner.selectedGenreList.append(NovelGenre.allCases[indexPath.row]) + owner.selectedGenreList.append(NovelGenre.detailSearchGenres[indexPath.row]) owner.selectedGenreListData.accept(owner.selectedGenreList) }) .disposed(by: disposeBag) @@ -221,12 +204,12 @@ final class DetailSearchViewModel: ViewModelType { input.genreColletionViewItemDeselected .subscribe(with: self, onNext: { owner, indexPath in owner.selectedGenreList = owner.selectedGenreListData.value - owner.selectedGenreList.removeAll { $0 == NovelGenre.allCases[indexPath.row] } + owner.selectedGenreList.removeAll { $0 == NovelGenre.detailSearchGenres[indexPath.row] } owner.selectedGenreListData.accept(owner.selectedGenreList) }) .disposed(by: disposeBag) - input.completedButtonDidTap + input.publicationStatusButtonDidTap .subscribe(with: self, onNext: { owner, selectedCompletedStatus in if owner.selectedCompletedStatus.value == selectedCompletedStatus { owner.selectedCompletedStatus.accept(nil) @@ -235,18 +218,16 @@ final class DetailSearchViewModel: ViewModelType { } }) .disposed(by: disposeBag) - - input.novelRatingButtonDidTap - .subscribe(with: self, onNext: { owner, selectedNovelRatingStatus in - if owner.selectedNovelRatingStatus.value == selectedNovelRatingStatus { - owner.selectedNovelRatingStatus.accept(nil) - } else { - owner.selectedNovelRatingStatus.accept(selectedNovelRatingStatus) - } + + input.ratingSliderValueChanged + .subscribe(with: self, onNext: { owner, range in + owner.selectedRatingLower.accept(range.0) + owner.selectedRatingUpper.accept(range.1) }) .disposed(by: disposeBag) + + // MARK: - 키워드 - // 키워드 input.updatedEnteredText .subscribe(with: self, onNext: { owner, text in owner.enteredText.accept(text) @@ -352,7 +333,7 @@ final class DetailSearchViewModel: ViewModelType { input.contactButtonDidTap .subscribe(with: self, onNext: { owner, _ in if let url = URL(string: ExternalLinks.inquiryAddNovel -) { + ) { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } @@ -360,11 +341,14 @@ final class DetailSearchViewModel: ViewModelType { }) .disposed(by: disposeBag) + let ratingRange = Observable + .combineLatest(selectedRatingLower, selectedRatingUpper) + let showInfoNewImageView = Observable .combineLatest( selectedGenreListData.map { $0.count > 0 }, selectedCompletedStatus.map { $0 != nil }, - selectedNovelRatingStatus.map { $0 != nil } + ratingRange.map { $0 != 0.0 || $1 != 5.0 } ) .map { $0 || $1 || $2 } @@ -372,13 +356,14 @@ final class DetailSearchViewModel: ViewModelType { .map { $0.count > 0 } .asObservable() - return Output(dismissModalViewController: dismissModalViewController.asObservable(), + return Output(popViewController: dismissModalViewController.asObservable(), selectedTab: selectedTab.asDriver(), showInfoNewImageView: showInfoNewImageView, showKeywordNewImageView: showKeywordNewImageView.asObservable(), + pushToResultViewController: pushToResultViewController.asObservable(), genreListData: genreListData.asObservable(), - selectedCompletedStatus: selectedCompletedStatus.asDriver(), - selectedNovelRatingStatus: selectedNovelRatingStatus.asDriver(), + selectedPublicationStatus: selectedCompletedStatus.asDriver(), + ratingRange: ratingRange.asDriver(onErrorJustReturn: (0.0, 5.0)), resetSelectedInfoData: resetSelectedInfoData.asObservable(), enteredText: enteredText.asObservable(), isKeywordTextFieldEditing: isKeywordTextFieldEditing.asObservable(), @@ -398,8 +383,3 @@ final class DetailSearchViewModel: ViewModelType { .observe(on: MainScheduler.instance) } } - -enum PreviousViewType { - case search - case resultSearchBar -} diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchHeaderView.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchHeaderView.swift index cbfa52c97..9b32d790c 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchHeaderView.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalAssistantView/NormalSearchHeaderView.swift @@ -48,7 +48,7 @@ final class NormalSearchHeaderView: UIView { $0.tintColor = .wssBlack $0.backgroundColor = .wssGray50 $0.textColor = .wssBlack - $0.placeholder = StringLiterals.NovelReview.KeywordSearch.placeholder + $0.placeholder = StringLiterals.Search.searchbar $0.font = .Body4 $0.layer.cornerRadius = 14 $0.layer.borderColor = UIColor.wssGray70.cgColor diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift index e2188e22a..7a4dead22 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchView/NormalSearchView.swift @@ -15,9 +15,10 @@ final class NormalSearchView: UIView { //MARK: - Components let headerView = NormalSearchHeaderView() + let sosoPickView = SearchSosoPickView() let resultView = NormalSearchResultView() let emptyView = NormalSearchEmptyView() - + let loadingView = WSSLoadingView() // MARK: - Life Cycle @@ -44,6 +45,7 @@ final class NormalSearchView: UIView { private func setHierarchy() { self.addSubviews(headerView, + sosoPickView, resultView, emptyView, loadingView) @@ -54,7 +56,12 @@ final class NormalSearchView: UIView { $0.top.equalTo(self.safeAreaLayoutGuide.snp.top).inset(1) $0.leading.trailing.equalToSuperview() } - + + sosoPickView.snp.makeConstraints { + $0.top.equalTo(headerView.snp.bottom).offset(16) + $0.leading.trailing.equalToSuperview() + } + resultView.snp.makeConstraints { $0.top.equalTo(headerView.snp.bottom) $0.horizontalEdges.bottom.equalToSuperview() diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift index b41603502..31fbc2042 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewController/NormalSearchViewController.swift @@ -86,6 +86,9 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { rootView.resultView.normalSearchCollectionView.register( NormalSearchCollectionViewCell.self, forCellWithReuseIdentifier: NormalSearchCollectionViewCell.cellIdentifier) + rootView.sosoPickView.sosopickCollectionView.register( + SosoPickCollectionViewCell.self, + forCellWithReuseIdentifier: SosoPickCollectionViewCell.cellIdentifier) } private func setDelegate() { @@ -114,7 +117,8 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { normalSearchCollectionViewContentSize: rootView.resultView.normalSearchCollectionView.rx.observe(CGSize.self, "contentSize"), normalSearchCellSelected: rootView.resultView.normalSearchCollectionView.rx.itemSelected, reachedBottom: reachedBottom, - normalSearchCollectionViewSwipeGesture: collectionViewSwipeGesture) + normalSearchCollectionViewSwipeGesture: collectionViewSwipeGesture, + sosoPickCellSelected: rootView.sosoPickView.sosopickCollectionView.rx.itemSelected) let output = viewModel.transform(from: input, disposeBag: disposeBag) output.resultCount @@ -140,16 +144,19 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { owner.rootView.emptyView.isHidden = true owner.rootView.resultView.isHidden = true owner.rootView.resultView.resultCountView.isHidden = true + owner.rootView.sosoPickView.isHidden = false } - else if novels.isEmpty && !(owner.rootView.headerView.searchTextField.text == "") { + else if novels.isEmpty { owner.rootView.emptyView.isHidden = false owner.rootView.resultView.isHidden = true owner.rootView.resultView.resultCountView.isHidden = true + owner.rootView.sosoPickView.isHidden = true } else { owner.rootView.emptyView.isHidden = true owner.rootView.resultView.isHidden = false owner.rootView.resultView.resultCountView.isHidden = false + owner.rootView.sosoPickView.isHidden = true } }) .disposed(by: disposeBag) @@ -218,6 +225,22 @@ final class NormalSearchViewController: UIViewController, UIScrollViewDelegate { owner.rootView.showLoadingView(isShow: isShow) }) .disposed(by: disposeBag) + + output.sosoPickList + .observe(on: MainScheduler.instance) + .bind(to: rootView.sosoPickView.sosopickCollectionView.rx.items( + cellIdentifier: SosoPickCollectionViewCell.cellIdentifier, + cellType: SosoPickCollectionViewCell.self)) { _, element, cell in + cell.bindData(data: element) + } + .disposed(by: disposeBag) + + output.presentToInduceLoginView + .observe(on: MainScheduler.instance) + .subscribe(with: self, onNext: { owner, _ in + owner.presentInduceLoginViewController() + }) + .disposed(by: disposeBag) } private func bindAction() { diff --git a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift index dae8aa171..084103209 100644 --- a/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift +++ b/WSSiOS/Source/Presentation/Search/NormalSearch/NormalSearchViewModel/NormalSearchViewModel.swift @@ -17,6 +17,8 @@ final class NormalSearchViewModel: ViewModelType { private let searchRepository: SearchRepository private let disposeBag = DisposeBag() + private let isLogined = APIConstants.isLogined + // API 쿼리 private let searchText = BehaviorRelay(value: "") private var currentPage: Int = 0 @@ -29,9 +31,13 @@ final class NormalSearchViewModel: ViewModelType { private let isSearchTextFieldEditing = BehaviorRelay(value: false) private let normalSearchList = BehaviorRelay<[SearchNovel]>(value: []) private let normalSearchCellIndexPath = PublishRelay() - + // 로딩 private let showLoadingView = PublishRelay() + + // 소소픽 + private let sosoPickList = BehaviorRelay<[SosoPickNovel]>(value: []) + private let presentToInduceLoginView = PublishRelay() //MARK: - Inputs @@ -48,6 +54,7 @@ final class NormalSearchViewModel: ViewModelType { let normalSearchCellSelected: ControlEvent let reachedBottom: Observable let normalSearchCollectionViewSwipeGesture: Observable + let sosoPickCellSelected: ControlEvent } //MARK: - Outputs @@ -65,6 +72,8 @@ final class NormalSearchViewModel: ViewModelType { let isSearchTextFieldEditing: Observable let endEditing: Observable let showLoadingView: Observable + let sosoPickList: Observable<[SosoPickNovel]> + let presentToInduceLoginView: Observable } //MARK: - init @@ -78,6 +87,10 @@ final class NormalSearchViewModel: ViewModelType { //MARK: - API + private func getSosoPickNovels() -> Observable { + return searchRepository.getSosoPickNovels() + } + private func getNormalSearchList(query: String, page: Int) -> Observable { return searchRepository.getSearchNovels(query: query, page: page) .do( @@ -110,7 +123,26 @@ final class NormalSearchViewModel: ViewModelType { //MARK: - Methods func transform(from input: Input, disposeBag: DisposeBag) -> Output { - + getSosoPickNovels() + .subscribe(with: self, onNext: { owner, data in + owner.sosoPickList.accept(data.sosoPicks) + }, onError: { _, error in + print(error.localizedDescription) + }) + .disposed(by: disposeBag) + + input.sosoPickCellSelected + .subscribe(with: self, onNext: { owner, indexPath in + AmplitudeManager.shared.track(AmplitudeEvent.Search.sosoPick) + if owner.isLogined { + let novelId = owner.sosoPickList.value[indexPath.row].novelId + owner.pushToNovelDetailViewController.accept(novelId) + } else { + owner.presentToInduceLoginView.accept(()) + } + }) + .disposed(by: disposeBag) + let searchRequest = Observable.merge(input.returnKeyDidTap.asObservable(), input.searchButtonDidTap.asObservable()) .withLatestFrom(input.searchTextUpdated) @@ -164,8 +196,12 @@ final class NormalSearchViewModel: ViewModelType { input.normalSearchCellSelected .subscribe(with: self, onNext: { owner, indexPath in AmplitudeManager.shared.track(AmplitudeEvent.Search.clickSearchResult) - let novelId = owner.normalSearchList.value[indexPath.row].novelId - owner.pushToNovelDetailViewController.accept(novelId) + if owner.isLogined { + let novelId = owner.normalSearchList.value[indexPath.row].novelId + owner.pushToNovelDetailViewController.accept(novelId) + } else { + owner.presentToInduceLoginView.accept(()) + } }) .disposed(by: disposeBag) @@ -196,6 +232,8 @@ final class NormalSearchViewModel: ViewModelType { pushToNovelDetailViewController: pushToNovelDetailViewController.asObservable(), isSearchTextFieldEditing: isSearchTextFieldEditing.asObservable(), endEditing: endEditing, - showLoadingView: showLoadingView.asObservable()) + showLoadingView: showLoadingView.asObservable(), + sosoPickList: sosoPickList.asObservable(), + presentToInduceLoginView: presentToInduceLoginView.asObservable()) } } diff --git a/WSSiOS/Source/Presentation/Search/Search/SearchView/SearchView.swift b/WSSiOS/Source/Presentation/Search/Search/SearchView/SearchView.swift index 64b8654d3..389733b84 100644 --- a/WSSiOS/Source/Presentation/Search/Search/SearchView/SearchView.swift +++ b/WSSiOS/Source/Presentation/Search/Search/SearchView/SearchView.swift @@ -20,8 +20,7 @@ final class SearchView: UIView { private let titleLabel = UILabel() let searchbarView = SearchBarView() let searchDetailInduceView = SearchDetailInduceView() - let sosopickView = SearchSosoPickView() - + private let loadingView = WSSLoadingView() // MARK: - Life Cycle @@ -61,8 +60,7 @@ final class SearchView: UIView { searchbarView, loadingView) scrollView.addSubview(contentView) - contentView.addSubviews(searchDetailInduceView, - sosopickView) + contentView.addSubviews(searchDetailInduceView) } private func setLayout() { @@ -94,11 +92,7 @@ final class SearchView: UIView { $0.top.equalToSuperview() $0.leading.trailing.equalToSuperview().inset(20) $0.height.equalTo(256) - } - - sosopickView.snp.makeConstraints { - $0.top.equalTo(searchDetailInduceView.snp.bottom).offset(24) - $0.leading.trailing.bottom.equalToSuperview() + $0.bottom.equalToSuperview().inset(24) } loadingView.snp.makeConstraints { diff --git a/WSSiOS/Source/Presentation/Search/Search/SearchViewController/SearchViewController.swift b/WSSiOS/Source/Presentation/Search/Search/SearchViewController/SearchViewController.swift index 00e3e3f44..d8d525d0c 100644 --- a/WSSiOS/Source/Presentation/Search/Search/SearchViewController/SearchViewController.swift +++ b/WSSiOS/Source/Presentation/Search/Search/SearchViewController/SearchViewController.swift @@ -18,8 +18,6 @@ final class SearchViewController: UIViewController { private let viewModel: SearchViewModel private let disposeBag = DisposeBag() - private let viewWillAppearEvent = PublishRelay() - //MARK: - Components private let rootView = SearchView() @@ -44,8 +42,7 @@ final class SearchViewController: UIViewController { showTabBar() setNavigationBar() - viewWillAppearEvent.accept(()) - + AmplitudeManager.shared.track(AmplitudeEvent.Search.search) } @@ -53,8 +50,6 @@ final class SearchViewController: UIViewController { super.viewDidLoad() setUI() - registerCell() - bindViewModel() } @@ -69,31 +64,15 @@ final class SearchViewController: UIViewController { } //MARK: - Bind - - private func registerCell() { - rootView.sosopickView.sosopickCollectionView.register( - SosoPickCollectionViewCell.self, - forCellWithReuseIdentifier: SosoPickCollectionViewCell.cellIdentifier) - } - + private func bindViewModel() { let input = SearchViewModel.Input( - viewWillAppearEvent: viewWillAppearEvent.asObservable(), searhBarDidTap: rootView.searchbarView.rx.tapGesture().when(.recognized).asObservable(), induceButtonDidTap: rootView.searchDetailInduceView.rx.tapGesture().when(.recognized).asObservable(), - sosoPickCellSelected: rootView.sosopickView.sosopickCollectionView.rx.itemSelected.asObservable(), pushToDetailSearchResultNotification: NotificationCenter.default.rx.notification(Notification.Name("PushToDetailSearchResult")).asObservable() ) let output = viewModel.transform(from: input, disposeBag: disposeBag) - output.sosoPickList - .bind(to: rootView.sosopickView.sosopickCollectionView.rx.items( - cellIdentifier: SosoPickCollectionViewCell.cellIdentifier, - cellType: SosoPickCollectionViewCell.self)) { row, element, cell in - cell.bindData(data: element) - } - .disposed(by: disposeBag) - output.pushToNormalSearchViewController .bind(with: self, onNext: { owner, _ in owner.pushToNormalSearchViewController() @@ -102,45 +81,14 @@ final class SearchViewController: UIViewController { output.pushToDetailSearchViewController .bind(with: self, onNext: { owner, _ in - owner.presentToDetailSearchViewController(selectedKeywordList: [], - previousViewInfo: .search, - selectedFilteredQuery: SearchFilterQuery(keywords: [], - genres: [], - isCompleted: nil, - novelRating: nil)) - }) - .disposed(by: disposeBag) - - output.pushToNovelDetailViewController - .bind(with: self, onNext: { owner, novelId in - owner.pushToNovelDetailViewController(novelId: novelId) + owner.pushToDetailSearchViewController() }) .disposed(by: disposeBag) output.pushToDetailSearchResultView .observe(on: MainScheduler.instance) .subscribe(with: self, onNext: { owner, notification in - if let userInfo = notification.userInfo { - let keywords = userInfo["keywords"] as? [KeywordData] - let genres = userInfo["genres"] as? [NovelGenre] - let isCompleted = userInfo["isCompleted"] as? Bool - let novelRating = userInfo["novelRating"] as? Float - - let detailSearchResultViewModel = DetailSearchResultViewModel( - searchRepository: DefaultSearchRepository(searchService: DefaultSearchService()), - keywords: keywords ?? [], - genres: genres ?? [], - isCompleted: isCompleted, - novelRating: novelRating - ) - - let detailSearchResultViewController = DetailSearchResultViewController(viewModel: detailSearchResultViewModel) - - detailSearchResultViewController.navigationController?.isNavigationBarHidden = false - detailSearchResultViewController.hidesBottomBarWhenPushed = true - - owner.navigationController?.pushViewController(detailSearchResultViewController, animated: true) - } + }) .disposed(by: disposeBag) @@ -151,11 +99,5 @@ final class SearchViewController: UIViewController { }) .disposed(by: disposeBag) - output.showLoadingView - .observe(on: MainScheduler.instance) - .bind(with: self, onNext: { owner, isShow in - owner.rootView.showLoadingView(isShow: isShow) - }) - .disposed(by: disposeBag) } } diff --git a/WSSiOS/Source/Presentation/Search/Search/SearchViewModel/SearchViewModel.swift b/WSSiOS/Source/Presentation/Search/Search/SearchViewModel/SearchViewModel.swift index 332bab51a..a1fe23b8f 100644 --- a/WSSiOS/Source/Presentation/Search/Search/SearchViewModel/SearchViewModel.swift +++ b/WSSiOS/Source/Presentation/Search/Search/SearchViewModel/SearchViewModel.swift @@ -11,48 +11,32 @@ import RxSwift import RxCocoa import RxGesture + final class SearchViewModel: ViewModelType { //MARK: - Properties - - private let searchRepository: SearchRepository + private let disposeBag = DisposeBag() - + private let isLogined = APIConstants.isLogined //MARK: - Inputs struct Input { - let viewWillAppearEvent: Observable let searhBarDidTap: Observable let induceButtonDidTap: Observable - let sosoPickCellSelected: Observable let pushToDetailSearchResultNotification: Observable } //MARK: - Outputs struct Output { - var sosoPickList = BehaviorRelay<[SosoPickNovel]>(value: []) let pushToNormalSearchViewController = PublishRelay() let pushToDetailSearchViewController = PublishRelay() - let pushToNovelDetailViewController = PublishRelay() let pushToDetailSearchResultView = PublishRelay() let presentToInduceLoginView = PublishRelay() - let showLoadingView = PublishRelay() } - //MARK: - init - - init(searchRepository: SearchRepository) { - self.searchRepository = searchRepository - } - - //MARK: - API - - func getSosoPickNovels() -> Observable { - return searchRepository.getSosoPickNovels() - } } //MARK: - Methods @@ -61,22 +45,6 @@ extension SearchViewModel { func transform(from input: Input, disposeBag: DisposeBag) -> Output { let output = Output() - input.viewWillAppearEvent - .flatMapLatest { - self.getSosoPickNovels() - } - .do(onNext: { _ in - output.showLoadingView.accept(true) - }) - .subscribe(with: self, onNext: { owner, data in - output.sosoPickList.accept(data.sosoPicks) - output.showLoadingView.accept(false) - }, onError: { owner, error in - print(error) - output.showLoadingView.accept(false) - }) - .disposed(by: disposeBag) - input.searhBarDidTap .subscribe(onNext: { _ in AmplitudeManager.shared.track(AmplitudeEvent.Search.generalSearch) @@ -99,18 +67,6 @@ extension SearchViewModel { }) .disposed(by: disposeBag) - input.sosoPickCellSelected - .subscribe(onNext: { indexPath in - AmplitudeManager.shared.track(AmplitudeEvent.Search.sosoPick) - if self.isLogined { - let novelId = output.sosoPickList.value[indexPath.row].novelId - output.pushToNovelDetailViewController.accept(novelId) - } else { - output.presentToInduceLoginView.accept(()) - } - }) - .disposed(by: disposeBag) - input.pushToDetailSearchResultNotification .subscribe(with: self, onNext: { owner, notification in output.pushToDetailSearchResultView.accept(notification) diff --git a/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageView/MyPageEditProfileView.swift b/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageView/MyPageEditProfileView.swift index 9ebc03dea..15f02bdd6 100644 --- a/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageView/MyPageEditProfileView.swift +++ b/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageView/MyPageEditProfileView.swift @@ -334,7 +334,8 @@ final class MyPageEditProfileView: UIView { genreCollectionView.snp.makeConstraints { $0.top.equalTo(genreDescriptionLabel.snp.bottom).offset(14) - $0.leading.trailing.equalToSuperview().inset(20) + $0.leading.equalToSuperview().inset(20) + $0.trailing.equalToSuperview().inset(30) $0.bottom.equalToSuperview() } } @@ -348,11 +349,11 @@ final class MyPageEditProfileView: UIView { completeButton.snp.makeConstraints { $0.width.equalTo(48) - $0.height.equalTo(42) + $0.height.equalTo(42).priority(.high) } - + backButton.snp.makeConstraints { - $0.size.equalTo(44) + $0.size.equalTo(44).priority(.high) } } } diff --git a/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewController/MyPageEditProfileViewController.swift b/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewController/MyPageEditProfileViewController.swift index a6b4fcf3b..b5c73dfea 100644 --- a/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewController/MyPageEditProfileViewController.swift +++ b/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewController/MyPageEditProfileViewController.swift @@ -103,7 +103,6 @@ final class MyPageEditProfileViewController: UIViewController { .bind(to: rootView.genreCollectionView.rx.items( cellIdentifier: MyPageEditProfileGenreCollectionViewCell.cellIdentifier, cellType: MyPageEditProfileGenreCollectionViewCell.self)) { row, element, cell in - print(element) cell.bindData(genre: element.0) cell.updateCell(isSelected: element.1) } @@ -246,7 +245,7 @@ extension MyPageEditProfileViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { var text: String? - let genreList: [String] = NovelGenre.allCases.map { $0.toKorean } + let genreList: [String] = NovelGenre.myPageEditGenres.map { $0.withKorean } text = genreList[indexPath.item] guard let unwrappedText = text else { @@ -254,6 +253,6 @@ extension MyPageEditProfileViewController: UICollectionViewDelegateFlowLayout { } let width = (unwrappedText as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.Body2]).width + 26 - return CGSize(width: width, height: 35) + return CGSize(width: width, height: 37) } } diff --git a/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewModel/MyPageEditProfileViewModel.swift b/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewModel/MyPageEditProfileViewModel.swift index 8202271c0..8e34f913d 100644 --- a/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewModel/MyPageEditProfileViewModel.swift +++ b/WSSiOS/Source/Presentation/UserPage/MyPage/MyPageViewModel/MyPageEditProfileViewModel.swift @@ -26,7 +26,7 @@ final class MyPageEditProfileViewModel: ViewModelType { private var avatarId: Int = -1 // 고정값 - private let genreList: [String] = NovelGenre.allCases.map { $0.toKorean } + private let genreList: [String] = NovelGenre.myPageEditGenres.map { $0.withKorean } private let nicknamePattern = "^[a-zA-Z0-9가-힣]{2,10}$" static let nicknameLimit = 10 static let introLimit = 50 @@ -293,7 +293,7 @@ final class MyPageEditProfileViewModel: ViewModelType { input.genreCellTap .bind(with: self, onNext: { owner, indexPath in let cellContent = owner.genreList[indexPath.row] - let toEnglish = NewNovelGenre.withKoreanRawValue(from: cellContent).rawValue + let toEnglish = NovelGenre.withKoreanRawValue(from: cellContent).rawValue let update = owner.checkGenreToUpdateCell(owner.userGenre.value, toEnglish) var updatedGenres = owner.userGenre.value @@ -314,7 +314,7 @@ final class MyPageEditProfileViewModel: ViewModelType { private func checkGenreToMakeTuple(_ totalGenre: [String], _ myGenre: [String]) -> [(String, Bool)] { let toKorean = myGenre.compactMap { genre in - NewNovelGenre(rawValue: genre)?.withKorean + NovelGenre(rawValue: genre)?.withKorean } return totalGenre.map { genre in