diff --git a/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift b/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift index 561e42c..64bad5e 100644 --- a/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift +++ b/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift @@ -1,5 +1,5 @@ // -// HorizontalMimScrollView.swift +// HorizontalMememScrollView.swift // DesignSystem // // Created by 리나 on 2024/06/29. @@ -8,14 +8,14 @@ import SwiftUI import ResourceKit -public protocol HorizontalMimItemProtocol: Hashable { } +public protocol HorizontalMemeItemProtocol: Hashable { } -public protocol HorizontalMimItemViewProtocol: View { - associatedtype Item: HorizontalMimItemProtocol +public protocol HorizontalMemeItemViewProtocol: View { + associatedtype Item: HorizontalMemeItemProtocol init(item: Item) } -public struct HorizontalMimScrollView: View where ItemView.Item == Item { +public struct HorizontalMemeScrollView: View where ItemView.Item == Item { @Binding public var items: [Item] private var itemClickHandler: ((Item) -> ())? public init(items: Binding<[Item]>, itemClickHandler: ((Item) -> ())? = nil) { diff --git a/Projects/Core/DesignSystem/Sources/CategoryTagView.swift b/Projects/Core/DesignSystem/Sources/KeywordsTagView.swift similarity index 77% rename from Projects/Core/DesignSystem/Sources/CategoryTagView.swift rename to Projects/Core/DesignSystem/Sources/KeywordsTagView.swift index 3f75de6..5d05ddd 100644 --- a/Projects/Core/DesignSystem/Sources/CategoryTagView.swift +++ b/Projects/Core/DesignSystem/Sources/KeywordsTagView.swift @@ -1,5 +1,5 @@ // -// CategoryTagView.swift +// KeywordsTagView.swift // DesignSystem // // Created by 리나 on 2024/06/29. @@ -9,18 +9,20 @@ import SwiftUI import ResourceKit // thanks to NamS -public struct CategoryTagView: View { - @State public var categories: [String] +public struct KeywordsTagView: View { + @State public var keywords: [String] + var onTapHandler: ((String) -> ())? - public init(categories: [String]) { - self.categories = categories + public init(keywords: [String], onTapHandler: ((String) -> ())?) { + self.keywords = keywords + self.onTapHandler = onTapHandler } public var body: some View { ScrollView { CategoryTagLayout(verticalSpacing: 8, horizontalSpacing: 8) { - ForEach(categories, id: \.self) { category in - Text(category) + ForEach(keywords, id: \.self) { keyword in + Text(keyword) .font(Font.Body.Medium.medium) .foregroundColor(Color.Text.primary) .padding(.horizontal, 16) @@ -28,14 +30,18 @@ public struct CategoryTagView: View { .background( Capsule().foregroundStyle(Color.Background.assistive) ) + .onTapGesture { + onTapHandler?(keyword) + } } } } .onAppear { + // tagView 사이즈를 잰 후 다시 그리기 위함 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - let cacheValue = categories - categories = [] - categories = cacheValue + let cacheValue = keywords + keywords = [] + keywords = cacheValue } } } diff --git a/Projects/Core/DesignSystem/Sources/MimCategoryView.swift b/Projects/Core/DesignSystem/Sources/MemeCategoryView.swift similarity index 50% rename from Projects/Core/DesignSystem/Sources/MimCategoryView.swift rename to Projects/Core/DesignSystem/Sources/MemeCategoryView.swift index 4dcad2b..4cb5b3b 100644 --- a/Projects/Core/DesignSystem/Sources/MimCategoryView.swift +++ b/Projects/Core/DesignSystem/Sources/MemeCategoryView.swift @@ -1,5 +1,5 @@ // -// MimCategoryView.swift +// MemeCategoryView.swift // DesignSystem // // Created by 리나 on 2024/06/30. @@ -8,19 +8,25 @@ import SwiftUI import ResourceKit -public struct MimCategoryView: View { - public let title: String - public let categories: [String] +public struct MemeCategoryView: View { + public let category: String + public let keywords: [String] + public let onTapHandler: ((String) -> ())? - public init(title: String, categories: [String]) { - self.title = title - self.categories = categories + public init( + category: String, + keywords: [String], + onTapHandler: ((String) -> ())? + ) { + self.category = category + self.keywords = keywords + self.onTapHandler = onTapHandler } public var body: some View { VStack(spacing: 0) { HStack { - Text(title) + Text(category) .font(Font.Body.Small.semiBold) .foregroundColor(Color.Text.tertiary) @@ -30,7 +36,7 @@ public struct MimCategoryView: View { .padding(.bottom, 16) .padding(.horizontal, 20) - CategoryTagView(categories: categories) + KeywordsTagView(keywords: keywords, onTapHandler: onTapHandler) .padding(.horizontal, 20) .padding(.bottom, 20) } diff --git a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift index 320de83..c5cde64 100644 --- a/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift +++ b/Projects/Core/DesignSystem/Sources/View/Meme/MemeListView.swift @@ -9,10 +9,14 @@ import SwiftUI import PPACModels public struct MemeListView: View { - private let memeDetailList: [MemeDetail] - private let columns = Array(repeating: GridItem(.flexible(), - spacing: 12, - alignment: .center),count: 2) + @Binding var memeDetailList: [MemeDetail] + private let columns = Array( + repeating: GridItem( + .flexible(), + spacing: 12, + alignment: .center + ), count: 2 + ) private let memeClickHandler: ((MemeDetail) -> ())? private let memeCopyHandler: ((MemeDetail) -> ())? private let onAppearLastMemeHandler: (() -> ())? @@ -22,12 +26,12 @@ public struct MemeListView: View { } public init( - memeDetailList: [MemeDetail], + memeDetailList: Binding<[MemeDetail]>, memeClickHandler: ((MemeDetail) -> ())? = nil, memeCopyHandler: ((MemeDetail) -> ())? = nil, onAppearLastMemeHandler: (() -> ())? = nil ) { - self.memeDetailList = memeDetailList + self._memeDetailList = memeDetailList self.memeClickHandler = memeClickHandler self.memeCopyHandler = memeCopyHandler self.onAppearLastMemeHandler = onAppearLastMemeHandler @@ -45,7 +49,6 @@ public struct MemeListView: View { } } - public var body: some View { ScrollView { HStack(alignment: .top) { @@ -96,11 +99,11 @@ public struct MemeListView: View { "https://images.unsplash.com/photo-1507146426996-ef05306b995a?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" ] - let memeDetailList = (0..<20) + @State var memeDetailList = (0..<20) .map { MemeDetail(id: "\($0)", title: MemeDetail.mock.title, keywords: MemeDetail.mock.keywords, imageUrlString: mockImageList[$0 % 4], source: MemeDetail.mock.source, isTodayMeme: true, reaction: $0 % 4, isFarmemed: true) } - return MemeListView(memeDetailList: memeDetailList) + return MemeListView(memeDetailList: $memeDetailList) } diff --git a/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift b/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift index e35c27e..27a3e7a 100644 --- a/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift +++ b/Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift @@ -1,22 +1,18 @@ -//// -//// MemeEndpoint.swift -//// PPACData -//// -//// Created by kimchansoo on 7/6/24. -//// // -//import Foundation +// MemeEndpoint.swift +// PPACData // -//import PPACNetwork +// Created by kimchansoo on 7/6/24. // import Foundation import PPACNetwork +import PPACUtil public enum MemeEndpoint: Requestable { - case recommendMeme(size: Int) + case getSearchKeywordMemeList(keyword: String) case meme(memeId: String) case bookmark(memeId: String) case share(memeId: String) @@ -31,6 +27,8 @@ public enum MemeEndpoint: Requestable { switch self { case .recommendMeme: return .get + case .getSearchKeywordMemeList: + return .get case .meme: return .get case .bookmark: @@ -52,6 +50,8 @@ public enum MemeEndpoint: Requestable { switch self { case .recommendMeme: return "/meme/recommend-memes" + case let .getSearchKeywordMemeList(keyword): + return "/meme/search/\(keyword)" case .meme(let memeId): return "/meme/\(memeId)" case .bookmark(let memeId): @@ -69,6 +69,8 @@ public enum MemeEndpoint: Requestable { switch self { case .recommendMeme(let size): return .query(["size": String(size)]) + case .getSearchKeywordMemeList: + return nil case .bookmark: return nil case .share: diff --git a/Projects/Core/PPACData/Sources/Repository/KeywordRepositoryImpl.swift b/Projects/Core/PPACData/Sources/Repository/KeywordRepositoryImpl.swift index f8ff3ad..ef99c2b 100644 --- a/Projects/Core/PPACData/Sources/Repository/KeywordRepositoryImpl.swift +++ b/Projects/Core/PPACData/Sources/Repository/KeywordRepositoryImpl.swift @@ -23,9 +23,8 @@ public final class KeywordRepositoryImpl: KeywordRepository { // MARK: - Methods public func getHotKeywords() async throws -> [HotKeyword] { - let result = await self.networkService - .request(KeywordEndPoint.getTopKeywords, - dataType: BaseDTO<[TopKeywordResponseDTO]>.self) + let endPoint = KeywordEndPoint.getTopKeywords + let result = await self.networkService.request(endPoint, dataType: BaseDTO<[TopKeywordResponseDTO]>.self) switch result { case .success(let data): guard let hotKeywordData = data.data else { throw NetworkError.dataDecodingError } @@ -37,16 +36,15 @@ public final class KeywordRepositoryImpl: KeywordRepository { } } - public func getMimCategorys() async throws -> [MimCategory] { - let result = await self.networkService - .request(KeywordEndPoint.getRecommendKeywords, - dataType: BaseDTO<[RecommendKeywordResponseDTO]>.self) + public func getMemeCategorys() async throws -> [MemeCategory] { + let endPoint = KeywordEndPoint.getRecommendKeywords + let result = await self.networkService.request(endPoint, dataType: BaseDTO<[RecommendKeywordResponseDTO]>.self) switch result { case .success(let data): - guard let mimCategoryData = data.data else { throw NetworkError.dataDecodingError } - let mimCategorys = mimCategoryData - .compactMap { MimCategory(title: $0.category, categories: $0.keywords.map {$0.name }) } - return mimCategorys + guard let memeCategoryData = data.data else { throw NetworkError.dataDecodingError } + let memeCategorys = memeCategoryData + .compactMap { MemeCategory(category: $0.category, keywords: $0.keywords.map { $0.name }) } + return memeCategorys case .failure(let error): throw error } diff --git a/Projects/Core/PPACData/Sources/Repository/MemeRepositoryImpl.swift b/Projects/Core/PPACData/Sources/Repository/MemeRepositoryImpl.swift index a4f4e48..99ab0ca 100644 --- a/Projects/Core/PPACData/Sources/Repository/MemeRepositoryImpl.swift +++ b/Projects/Core/PPACData/Sources/Repository/MemeRepositoryImpl.swift @@ -38,6 +38,18 @@ public class MemeRepositoryImpl: MemeRepository { } } + public func getSearchKeywordMemeList(keyword: String) async throws -> [MemeDetail] { + let endpoint = MemeEndpoint.getSearchKeywordMemeList(keyword: keyword) + let result = await networkservice.request(endpoint, dataType: BaseDTO.self) + switch result { + case .success(let data): + guard let memeResponseDTOList = data.data?.memeList else { throw NetworkError.dataDecodingError } + return memeResponseDTOList.map { $0.toModel() } + case .failure(let error): + throw error + } + } + public func getMemeDetail(memeId: String) async throws -> MemeDetail { let endpoint = MemeEndpoint.meme(memeId: memeId) let result = await networkservice.request(endpoint, dataType: BaseDTO.self) diff --git a/Projects/Core/PPACDomain/Sources/Repository/KeywordRepository.swift b/Projects/Core/PPACDomain/Sources/Repository/KeywordRepository.swift index a39f055..92717cf 100644 --- a/Projects/Core/PPACDomain/Sources/Repository/KeywordRepository.swift +++ b/Projects/Core/PPACDomain/Sources/Repository/KeywordRepository.swift @@ -10,5 +10,5 @@ import PPACModels public protocol KeywordRepository { func getHotKeywords() async throws -> [HotKeyword] - func getMimCategorys() async throws -> [MimCategory] + func getMemeCategorys() async throws -> [MemeCategory] } diff --git a/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift b/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift index 8397ef1..ec39a00 100644 --- a/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift +++ b/Projects/Core/PPACDomain/Sources/Repository/MemeRepository.swift @@ -12,6 +12,7 @@ import PPACModels public protocol MemeRepository { func getRecommendMemes(size: Int) async throws -> [MemeDetail] + func getSearchKeywordMemeList(keyword: String) async throws -> [MemeDetail] func getMemeDetail(memeId: String) async throws -> MemeDetail func bookmarkMeme(memeId: String) async throws func shareMeme(memeId: String) async throws diff --git a/Projects/Core/PPACDomain/Sources/Meme/BookmarkMemeUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/BookmarkMemeUseCase.swift similarity index 100% rename from Projects/Core/PPACDomain/Sources/Meme/BookmarkMemeUseCase.swift rename to Projects/Core/PPACDomain/Sources/UseCase/Meme/BookmarkMemeUseCase.swift diff --git a/Projects/Core/PPACDomain/Sources/Meme/GetMemeDetailUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/GetMemeDetailUseCase.swift similarity index 100% rename from Projects/Core/PPACDomain/Sources/Meme/GetMemeDetailUseCase.swift rename to Projects/Core/PPACDomain/Sources/UseCase/Meme/GetMemeDetailUseCase.swift diff --git a/Projects/Core/PPACDomain/Sources/Meme/GetRecommendMemesUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/GetRecommendMemesUseCase.swift similarity index 100% rename from Projects/Core/PPACDomain/Sources/Meme/GetRecommendMemesUseCase.swift rename to Projects/Core/PPACDomain/Sources/UseCase/Meme/GetRecommendMemesUseCase.swift diff --git a/Projects/Core/PPACDomain/Sources/Meme/ReactToMemeUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/ReactToMemeUseCase.swift similarity index 100% rename from Projects/Core/PPACDomain/Sources/Meme/ReactToMemeUseCase.swift rename to Projects/Core/PPACDomain/Sources/UseCase/Meme/ReactToMemeUseCase.swift diff --git a/Projects/Core/PPACDomain/Sources/UseCase/Meme/SearchKeywordUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/SearchKeywordUseCase.swift new file mode 100644 index 0000000..a87c666 --- /dev/null +++ b/Projects/Core/PPACDomain/Sources/UseCase/Meme/SearchKeywordUseCase.swift @@ -0,0 +1,26 @@ +// +// SearchKeywordUseCase.swift +// PPACDomain +// +// Created by 리나 on 7/18/24. +// + +import Foundation + +import PPACModels + +public protocol SearchKeywordUseCase { + func execute(keyword: String) async throws -> [MemeDetail] +} + +public class SearchKeywordUseCaseImpl: SearchKeywordUseCase { + private let repository: MemeRepository + + public init(repository: MemeRepository) { + self.repository = repository + } + + public func execute(keyword: String) async throws -> [MemeDetail] { + try await repository.getSearchKeywordMemeList(keyword: keyword) + } +} diff --git a/Projects/Core/PPACDomain/Sources/Meme/ShareMemeUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/ShareMemeUseCase.swift similarity index 100% rename from Projects/Core/PPACDomain/Sources/Meme/ShareMemeUseCase.swift rename to Projects/Core/PPACDomain/Sources/UseCase/Meme/ShareMemeUseCase.swift diff --git a/Projects/Core/PPACDomain/Sources/Meme/WatchMemeUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Meme/WatchMemeUseCase.swift similarity index 100% rename from Projects/Core/PPACDomain/Sources/Meme/WatchMemeUseCase.swift rename to Projects/Core/PPACDomain/Sources/UseCase/Meme/WatchMemeUseCase.swift diff --git a/Projects/Core/PPACDomain/Sources/UseCase/Search/HotKeywordsUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Search/HotKeywordsUseCase.swift new file mode 100644 index 0000000..9975371 --- /dev/null +++ b/Projects/Core/PPACDomain/Sources/UseCase/Search/HotKeywordsUseCase.swift @@ -0,0 +1,26 @@ +// +// HotKeywordsUseCase.swift +// PPACDomain +// +// Created by 리나 on 7/18/24. +// + +import Foundation + +import PPACModels + +public protocol HotKeywordsUseCase { + func execute() async throws -> [HotKeyword] +} + +public class HotKeywordsUseCaseImpl: HotKeywordsUseCase { + private let repository: KeywordRepository + + public init(repository: KeywordRepository) { + self.repository = repository + } + + public func execute() async throws -> [HotKeyword] { + try await repository.getHotKeywords() + } +} diff --git a/Projects/Core/PPACDomain/Sources/UseCase/Search/MemeCategorysUseCase.swift b/Projects/Core/PPACDomain/Sources/UseCase/Search/MemeCategorysUseCase.swift new file mode 100644 index 0000000..a69b0d0 --- /dev/null +++ b/Projects/Core/PPACDomain/Sources/UseCase/Search/MemeCategorysUseCase.swift @@ -0,0 +1,26 @@ +// +// MemeCategorysUseCase.swift +// PPACDomain +// +// Created by 리나 on 7/18/24. +// + +import Foundation + +import PPACModels + +public protocol MemeCategorysUseCase { + func execute() async throws -> [MemeCategory] +} + +public class MemeCategorysUseCaseImpl: MemeCategorysUseCase { + private let repository: KeywordRepository + + public init(repository: KeywordRepository) { + self.repository = repository + } + + public func execute() async throws -> [MemeCategory] { + try await repository.getMemeCategorys() + } +} diff --git a/Projects/Core/PPACModels/Sources/Search/MemeCategory.swift b/Projects/Core/PPACModels/Sources/Search/MemeCategory.swift new file mode 100644 index 0000000..4d5d125 --- /dev/null +++ b/Projects/Core/PPACModels/Sources/Search/MemeCategory.swift @@ -0,0 +1,20 @@ +// +// MemeCategory.swift +// PPACModels +// +// Created by 리나 on 2024/06/29. +// + +import Foundation + +public struct MemeCategory: Hashable, Identifiable { + public let id = UUID() + public let category: String + public let keywords: [String] + + public init(category: String, keywords: [String]) { + self.category = category + self.keywords = keywords + } +} + diff --git a/Projects/Core/PPACModels/Sources/Search/MimCategory.swift b/Projects/Core/PPACModels/Sources/Search/MimCategory.swift deleted file mode 100644 index eff6a55..0000000 --- a/Projects/Core/PPACModels/Sources/Search/MimCategory.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MimCategory.swift -// PPACModels -// -// Created by 리나 on 2024/06/29. -// - -import Foundation - -public struct MimCategory: Hashable, Identifiable { - public let id = UUID() - public let title: String - public let categories: [String] - - public init(title: String, categories: [String]) { - self.title = title - self.categories = categories - } -} - -public extension MimCategory { - static let mock1 = MimCategory( - title: "감정", - categories: ["행복", "슬픈", "무념무상", "분노", "웃긴", "피곤", "절망", "현타", "당황"] - ) - static let mock2 = MimCategory( - title: "상황", - categories: ["회사", "대학", "공부", "연애", "긁", "다이어트", "주식", "고민", "축하", "칭찬"] - ) - static let mock3 = MimCategory( - title: "컨텐츠", - categories: ["동물", "무한도전", "캐릭터", "짱구", "문상훈", "검정고무신", "그 외"] - ) -} diff --git a/Projects/DemoApp/Sources/DemoApp.swift b/Projects/DemoApp/Sources/DemoApp.swift index ae1e958..6cae271 100644 --- a/Projects/DemoApp/Sources/DemoApp.swift +++ b/Projects/DemoApp/Sources/DemoApp.swift @@ -9,19 +9,26 @@ import SwiftUI import Home import MemeDetail +import Search +import PPACDomain +import PPACNetwork +import PPACData @main struct DemoApp: App { enum Views: String, CaseIterable, Identifiable { case MemeDetail + case SearchResult var id: String { self.rawValue } - var view: some View { + var view: AnyView { switch self { case .MemeDetail: - getMemeDetailView() + AnyView(getMemeDetailView()) + case .SearchResult: + AnyView(getSearchResultView()) } } } @@ -49,8 +56,22 @@ private extension DemoApp.Views { viewModel: MemeDetailViewModel( meme: .mock, router: nil, - copyImageUseCase: CopyImageUseCaseImpl(), - postLikeUseCase: PostLikeUseCaseImpl() + bookmarkMemeUseCase: BookmarkMemeUseCaseImpl(repository: MemeRepositoryImpl(networkservice: NetworkService())), + shareMemeUseCase: ShareMemeUseCaseImpl(repository: MemeRepositoryImpl(networkservice: NetworkService())), + watchMemeUseCase: WatchMemeUseCaseImpl(repository: MemeRepositoryImpl(networkservice: NetworkService())), + reactToMemeUseCase: ReactToMemeUseCaseImpl(repository: MemeRepositoryImpl(networkservice: NetworkService())) + ) + ) + } + + func getSearchResultView() -> some View { + return SearchResultView( + viewModel: SearchResultViewModel( + keyword: "긁", + router: nil, + searchKeywordUseCase: SearchKeywordUseCaseImpl( + repository: MemeRepositoryImpl(networkservice: NetworkService()) + ), copyImageUseCase: CopyImageUseCaseImpl() ) ) } diff --git a/Projects/Features/Home/Sources/Presentation/TabRouter.swift b/Projects/Features/Home/Sources/Presentation/TabRouter.swift index 84beeff..0293645 100644 --- a/Projects/Features/Home/Sources/Presentation/TabRouter.swift +++ b/Projects/Features/Home/Sources/Presentation/TabRouter.swift @@ -57,10 +57,9 @@ public final class MainTabRouter: Router, ObservableObject { childRouters.append(recommendRouter) recommendRouter.start() case .search: - // sample - let detailRouter = MemeDetailRouter(self.navigationController, meme: .mock) - childRouters.append(detailRouter) - detailRouter.start() + let searchRouter = SearchRouter(self.navigationController, selectedTab: selectedTabBinding) + childRouters.append(searchRouter) + searchRouter.start() case .mypage: let myPageRouter = MyPageRouter(navigationController: self.navigationController, selectedTab: selectedTabBinding, diff --git a/Projects/Features/MyPage/Sources/Components/RecentlyMemeListView.swift b/Projects/Features/MyPage/Sources/Components/RecentlyMemeListView.swift index b2cf6ed..14a091c 100644 --- a/Projects/Features/MyPage/Sources/Components/RecentlyMemeListView.swift +++ b/Projects/Features/MyPage/Sources/Components/RecentlyMemeListView.swift @@ -27,10 +27,9 @@ struct RecentlyMemeListView: View { } } - var memeListView: some View { VStack { - HorizontalMimScrollView( + HorizontalMemeScrollView( items: $memeDetailList, itemClickHandler: memeClickHandler ) @@ -42,12 +41,11 @@ struct RecentlyMemeListView: View { var emptyView: some View { MemeListEmptyView(description: "최근 본 밈이 없어요") } - } -extension MemeDetail: HorizontalMimItemProtocol {} +extension MemeDetail: HorizontalMemeItemProtocol {} -struct MemeSimpleItemView: View, HorizontalMimItemViewProtocol { +struct MemeSimpleItemView: View, HorizontalMemeItemViewProtocol { typealias Item = MemeDetail let memeDetail: MemeDetail diff --git a/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift b/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift index 7e46557..4146edb 100644 --- a/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift +++ b/Projects/Features/MyPage/Sources/Components/SavedMemeListView.swift @@ -32,7 +32,7 @@ struct SavedMemeListView: View { var memeListView: some View { VStack { MemeListView( - memeDetailList: memeDetailList, + memeDetailList: $memeDetailList, memeClickHandler: memeClickHandler, memeCopyHandler: memeCopyHandler, onAppearLastMemeHandler: onAppearLastMemeHandler diff --git a/Projects/Features/Search/Project.swift b/Projects/Features/Search/Project.swift index 17f30fb..43dacaa 100644 --- a/Projects/Features/Search/Project.swift +++ b/Projects/Features/Search/Project.swift @@ -21,6 +21,8 @@ let project = Project( .ResourceKit, .Core.DesignSystem, .Core.PPACModels, + .Core.PPACData, + .Core.PPACNetwork, .ThirdParty.Dependency, .ThirdParty.Kingfisher, ] diff --git a/Projects/Features/Search/Sources/View/HotKeywordImageView.swift b/Projects/Features/Search/Sources/View/HotKeywordImageView.swift index e2442e4..9f42e0e 100644 --- a/Projects/Features/Search/Sources/View/HotKeywordImageView.swift +++ b/Projects/Features/Search/Sources/View/HotKeywordImageView.swift @@ -11,7 +11,7 @@ import PPACModels import DesignSystem import ResourceKit -struct HotKeywordImageView: View, HorizontalMimItemViewProtocol { +struct HotKeywordImageView: View, HorizontalMemeItemViewProtocol { typealias Item = HotKeyword let hotKeyword: HotKeyword diff --git a/Projects/Features/Search/Sources/View/Result/SearchResultRouter.swift b/Projects/Features/Search/Sources/View/Result/SearchResultRouter.swift new file mode 100644 index 0000000..2ad1161 --- /dev/null +++ b/Projects/Features/Search/Sources/View/Result/SearchResultRouter.swift @@ -0,0 +1,56 @@ +// +// SearchResultRouter.swift +// Search +// +// Created by 리나 on 7/13/24. +// + +import UIKit +import SwiftUI + +import PPACUtil +import PPACModels +import PPACData +import PPACNetwork +import PPACDomain +import DesignSystem +import MemeDetail + +public final class SearchResultRouter: Router, SearchResultRouting { + + // MARK: - Properties + public var delegate: (any RouterDelegate)? + public var navigationController: UINavigationController + public var childRouters: [any Router] = [] + + let keyword: String + + // MARK: - Initializers + + public init(_ navigationController: UINavigationController, keyword: String) { + navigationController.isNavigationBarHidden = true + self.navigationController = navigationController + self.keyword = keyword + } + + // MARK: - Methods + + public func start() { + let repository = MemeRepositoryImpl(networkservice: NetworkService()) + + self.pushView( + SearchResultView(viewModel: SearchResultViewModel( + keyword: keyword, + router: self, + searchKeywordUseCase: SearchKeywordUseCaseImpl(repository: repository), + copyImageUseCase: CopyImageUseCaseImpl() + )) + ) + } + + public func showMemeDetail(memeDetail: MemeDetail) { + let router = MemeDetailRouter(self.navigationController, meme: memeDetail) + self.childRouters.append(router) + router.start() + } +} diff --git a/Projects/Features/Search/Sources/View/Result/SearchResultView.swift b/Projects/Features/Search/Sources/View/Result/SearchResultView.swift new file mode 100644 index 0000000..ad975c3 --- /dev/null +++ b/Projects/Features/Search/Sources/View/Result/SearchResultView.swift @@ -0,0 +1,62 @@ +// +// SearchResultView.swift +// Search +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI + +import PPACModels +import ResourceKit +import DesignSystem + +public struct SearchResultView: View { + @ObservedObject var viewModel: SearchResultViewModel + + public init(viewModel: SearchResultViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Text("\(viewModel.state.memeList.count)개의 밈을 찾았어요") + .font(Font.Body.Medium.medium) + .foregroundColor(Color.Text.primary) + .padding(.all, 20) + + MemeListView( + memeDetailList: $viewModel.state.memeList, + memeClickHandler: { meme in + viewModel.dispatch(type: .memeDetailTapped(meme: meme)) + }, + memeCopyHandler: { meme in + viewModel.dispatch(type: .memeCopyTapped(meme: meme)) + } + ) + .padding(.horizontal, 20) + + HStack(alignment: .center) { + Text("카페인 빨리 충전하고\n재밌는 밈 더 찾아둘게요!") + .font(Font.Body.Large.medium) + .foregroundColor(Color.Text.assistive) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .frame(height: 236) + } + + } + .padding(.top, 51) + .plainNavigationBar( + backHandler: { viewModel.dispatch(type: .naviBackButtonTapped) }, + rightActionHandler: nil, + hasConfigureButton: false, + title: "키워드" + ) + .onAppear { + viewModel.dispatch(type: .viewWillAppear) + } + } +} diff --git a/Projects/Features/Search/Sources/View/Result/SearchResultViewModel.swift b/Projects/Features/Search/Sources/View/Result/SearchResultViewModel.swift new file mode 100644 index 0000000..2c732bc --- /dev/null +++ b/Projects/Features/Search/Sources/View/Result/SearchResultViewModel.swift @@ -0,0 +1,96 @@ +// +// SearchResultViewModel.swift +// Search +// +// Created by 리나 on 7/13/24. +// + +import UIKit +import Dependencies + +import PPACUtil +import PPACModels +import PPACDomain +import PPACNetwork +import PPACData +import MemeDetail + +@MainActor +public protocol SearchResultRouting: AnyObject { + func popView() + func showMemeDetail(memeDetail: MemeDetail) +} + +public final class SearchResultViewModel: ViewModelType, ObservableObject { + + public enum Action { + case viewWillAppear + case memeDetailTapped(meme: MemeDetail) + case memeCopyTapped(meme: MemeDetail) + case naviBackButtonTapped + } + + public struct State { + var keyword: String + var memeList: [MemeDetail] + } + + // MARK: - Properties + + weak var router: SearchResultRouting? + @Published public var state: State + + private let searchKeywordUseCase: SearchKeywordUseCase + private let copyImageUseCase: CopyImageUseCase + + // MARK: - Initializers + + public init( + keyword: String, + router: SearchResultRouting?, + searchKeywordUseCase: SearchKeywordUseCase, + copyImageUseCase: CopyImageUseCase + ) { + self.router = router + self.state = State(keyword: keyword, memeList: []) + self.searchKeywordUseCase = searchKeywordUseCase + self.copyImageUseCase = copyImageUseCase + } + + // MARK: - Methods + + @MainActor + public func dispatch(type: Action) { + Task { @MainActor in + switch type { + case .viewWillAppear: + await fetchData() + case .memeDetailTapped(let meme): + router?.showMemeDetail(memeDetail: meme) + case .memeCopyTapped(let meme): + await copyImage(urlString: meme.imageUrlString) + break + case .naviBackButtonTapped: + router?.popView() + } + } + } + + @MainActor + private func fetchData() async { + do { + state.memeList = try await searchKeywordUseCase.execute(keyword: state.keyword) + } catch(let error) { + debugPrint("error = \(error)") + } + } + + @MainActor + private func copyImage(urlString: String) async { + do { + try await copyImageUseCase.execute(url: urlString) + } catch(let error) { + debugPrint("error = \(error)") + } + } +} diff --git a/Projects/Features/Search/Sources/View/SearchRouter.swift b/Projects/Features/Search/Sources/View/SearchRouter.swift new file mode 100644 index 0000000..3bd2e55 --- /dev/null +++ b/Projects/Features/Search/Sources/View/SearchRouter.swift @@ -0,0 +1,54 @@ +// +// SearchRouter.swift +// Search +// +// Created by 리나 on 7/17/24. +// + +import UIKit +import SwiftUI + +import PPACUtil +import PPACModels +import PPACData +import PPACDomain +import PPACNetwork +import DesignSystem + +public final class SearchRouter: Router, SearchRouting { + + // MARK: - Properties + public var delegate: (any RouterDelegate)? + public var navigationController: UINavigationController + public var childRouters: [any Router] = [] + private var selectedTab: Binding + + // MARK: - Initializers + + public init(_ navigationController: UINavigationController, selectedTab: Binding) { + navigationController.isNavigationBarHidden = true + self.navigationController = navigationController + self.selectedTab = selectedTab + } + + // MARK: - Methods + + public func start() { + let repository = KeywordRepositoryImpl(networkService: NetworkService()) + + let view = SearchView( + viewModel: SearchViewModel( + router: self, + hotKeywordsUseCase: HotKeywordsUseCaseImpl(repository: repository), + memeCategorysUseCase: MemeCategorysUseCaseImpl(repository: repository) + ) + ).tabBar(selectedTab: selectedTab) + setRootView(view) + } + + public func showSearchResult(keyword: String) { + let router = SearchResultRouter(self.navigationController, keyword: keyword) + self.childRouters.append(router) + router.start() + } +} diff --git a/Projects/Features/Search/Sources/View/SearchView.swift b/Projects/Features/Search/Sources/View/SearchView.swift index b8b1b67..347dfa7 100644 --- a/Projects/Features/Search/Sources/View/SearchView.swift +++ b/Projects/Features/Search/Sources/View/SearchView.swift @@ -6,18 +6,19 @@ // import SwiftUI + import PPACModels -import ResourceKit +import PPACData +import PPACNetwork +import PPACUtil import DesignSystem +import ResourceKit public struct SearchView: View { - @State public var hotKeywords: [HotKeyword] - @State public var mimCategories: [MimCategory] - @State private var isPresenting: Bool = false + @ObservedObject var viewModel: SearchViewModel - public init(hotKeywords: [HotKeyword] = [], mimCategories: [MimCategory] = []) { - self.hotKeywords = hotKeywords - self.mimCategories = mimCategories + public init(viewModel: SearchViewModel) { + self.viewModel = viewModel } public var body: some View { @@ -27,24 +28,29 @@ public struct SearchView: View { ScrollView { VStack(spacing: 0) { currentHotKeywords - mimCategoriesViews + memeCategoriesViews } } .scrollIndicators(.hidden) } .padding(.bottom, 40) + .onAppear { + viewModel.dispatch(type: .viewWillAppear) + } .basicModal( - isPresented: $isPresenting, + isPresented: $viewModel.state.isPresenting, opacity: 0.5, content: { - SearchPreparingAlert(dismiss: { isPresenting = false }) + SearchPreparingAlert { + viewModel.dispatch(type: .dismissSearchBarAlert) + } } ) } private var fakeSearchBar: some View { Button { - isPresenting = true + viewModel.dispatch(type: .searchBarTapped) } label: { FakeSearchBar(placeHolder: "🚧 검색은 오픈 준비 중!") } @@ -66,13 +72,15 @@ public struct SearchView: View { .padding(.horizontal, 20) .padding(.vertical, 18) - HorizontalMimScrollView(items: $hotKeywords) - .frame(height: 90) + HorizontalMemeScrollView(items: $viewModel.state.hotKeywords) { hotKeyword in + viewModel.dispatch(type: .hotKeywordTapped(keyword: hotKeyword.title)) + } + .frame(height: 90) } .padding(.bottom, 40) } - - private var mimCategoriesViews: some View { + + private var memeCategoriesViews: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 8) { ResourceKitAsset.Icon.category.swiftUIImage @@ -83,30 +91,23 @@ public struct SearchView: View { Text("무슨 밈 찾아?") .font(Font.Heading.Small.semiBold) .foregroundColor(Color.Text.primary) + + Spacer() } .padding(.horizontal, 20) .padding(.vertical, 18) - ForEach(mimCategories, id: \.self) { mimCategory in - MimCategoryView(title: mimCategory.title, categories: mimCategory.categories) + ForEach(viewModel.state.memeCategories, id: \.self) { memeCategory in + MemeCategoryView( + category: memeCategory.category, + keywords: memeCategory.keywords + ) { keyword in + viewModel.dispatch(type: .recommendKeywordTapped(keyword: keyword)) + } } } } } -extension HotKeyword: HorizontalMimItemProtocol {} +extension HotKeyword: HorizontalMemeItemProtocol {} -#Preview { - SearchView( - hotKeywords: [ - .mock1, - .mock2, - .mock3, - .mock4 - ], mimCategories: [ - .mock1, - .mock2, - .mock3 - ] - ) -} diff --git a/Projects/Features/Search/Sources/View/SearchViewModel.swift b/Projects/Features/Search/Sources/View/SearchViewModel.swift new file mode 100644 index 0000000..4632ac2 --- /dev/null +++ b/Projects/Features/Search/Sources/View/SearchViewModel.swift @@ -0,0 +1,91 @@ +// +// SearchViewModel.swift +// Search +// +// Created by 리나 on 7/18/24. +// + +import UIKit +import Dependencies + +import PPACUtil +import PPACModels +import PPACDomain +import PPACNetwork +import PPACData +import MemeDetail + +@MainActor +public protocol SearchRouting: AnyObject { + func showSearchResult(keyword: String) +} + +public final class SearchViewModel: ViewModelType, ObservableObject { + + public enum Action { + case viewWillAppear + case searchBarTapped + case dismissSearchBarAlert + case hotKeywordTapped(keyword: String) + case recommendKeywordTapped(keyword: String) + } + + public struct State { + var hotKeywords: [HotKeyword] + var memeCategories: [MemeCategory] + var isPresenting: Bool = false + } + + // MARK: - Properties + + weak var router: SearchRouting? + @Published public var state: State + + private let hotKeywordsUseCase: HotKeywordsUseCase + private let memeCategorysUseCase: MemeCategorysUseCase + + // MARK: - Initializers + + public init( + router: SearchRouting?, + hotKeywordsUseCase: HotKeywordsUseCase, + memeCategorysUseCase: MemeCategorysUseCase + ) { + self.router = router + self.state = State(hotKeywords: [], memeCategories: []) + self.hotKeywordsUseCase = hotKeywordsUseCase + self.memeCategorysUseCase = memeCategorysUseCase + } + + // MARK: - Methods + + @MainActor + public func dispatch(type: Action) { + Task { @MainActor in + switch type { + case .viewWillAppear: + await fetchData() + case .searchBarTapped: + state.isPresenting = true + case .dismissSearchBarAlert: + state.isPresenting = false + case .hotKeywordTapped(let keyword): + router?.showSearchResult(keyword: keyword) + case .recommendKeywordTapped(let keyword): + router?.showSearchResult(keyword: keyword) + } + } + } + + @MainActor + func fetchData() async { + guard state.hotKeywords == [] || state.memeCategories == [] else { return } + + do { + state.hotKeywords = try await hotKeywordsUseCase.execute() + state.memeCategories = try await memeCategorysUseCase.execute() + } catch(let error) { + debugPrint("error = \(error)") + } + } +}