Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 검색 기능 추가 #89

Merged
merged 1 commit into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Projects/Core/PPACData/Sources/DTO/MemeResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,31 +45,31 @@ extension MemeWithPaginationResponseDTO {
struct MemeResponseDTO: Decodable {
let _id: String
let title: String
let keywords: [KeywordResponseDTO]
let keywords: [KeywordResponseDTO]?
let image: String
let reaction: Int
let source: String
let isTodayMeme: Bool
let isDeleted: Bool?
let createdAt: String?
let updatedAt: String
let isSaved: Bool
let isReaction: Bool
let isSaved: Bool?
let isReaction: Bool?
let watch: Int?

public init(
_id: String,
title: String,
keywords: [KeywordResponseDTO],
keywords: [KeywordResponseDTO]?,
image: String,
reaction: Int,
source: String,
isTodayMeme: Bool,
isDeleted: Bool?,
createdAt: String?,
updatedAt: String,
isSaved: Bool,
isReaction: Bool,
isSaved: Bool?,
isReaction: Bool?,
watch: Int?
)
{
Expand Down Expand Up @@ -108,13 +108,13 @@ extension MemeResponseDTO {
return MemeDetail(
id: self._id,
title: self.title,
keywords: self.keywords.map { $0.name },
keywords: self.keywords?.compactMap { $0.name } ?? [],
imageUrlString: self.image,
source: self.source,
isTodayMeme: self.isTodayMeme,
reaction: self.reaction,
isFarmemed: self.isSaved,
isReaction: self.isReaction
isFarmemed: self.isSaved ?? false,
isReaction: self.isReaction ?? false
)
}
}
12 changes: 12 additions & 0 deletions Projects/Core/PPACData/Sources/Endpoint/MemeEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import PPACUtil
public enum MemeEndpoint: Requestable {
case recommendMeme(size: Int)
case getSearchKeywordMemeList(page: Int, size: Int, keyword: String)
case getSearchByTextMemeList(page: Int, size: Int, text: String)
case meme(memeId: String)
case bookmark(memeId: String)
case deleteBookmark(memeId: String)
Expand All @@ -26,6 +27,8 @@ public enum MemeEndpoint: Requestable {
return .get
case .getSearchKeywordMemeList:
return .get
case .getSearchByTextMemeList:
return .get
case .meme:
return .get
case .bookmark:
Expand All @@ -51,6 +54,8 @@ public enum MemeEndpoint: Requestable {
return "/meme/recommend-memes"
case .getSearchKeywordMemeList(_,_,let keyword):
return "/meme/search/\(keyword)"
case .getSearchByTextMemeList(_,_,let text):
return "/meme/search"
case .meme(let memeId):
return "/meme/\(memeId)"
case .bookmark(let memeId):
Expand All @@ -77,6 +82,13 @@ public enum MemeEndpoint: Requestable {
"keyword" : "\(keyword)"
]
return .query(parameters)
case .getSearchByTextMemeList(let page, let size, let text):
let parameters: [String: String] = [
"q": "\(text)",
"page" : "\(page)",
"size" : "\(size)",
]
return .query(parameters)
case .bookmark:
return nil
case .deleteBookmark:
Expand Down
17 changes: 17 additions & 0 deletions Projects/Core/PPACData/Sources/Repository/MemeRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ public class MemeRepositoryImpl: MemeRepository {
}
}

public func getSearchByTextMemeList(
page: Int,
size: Int,
text: String
) async throws -> MemeListWithPagination {
let endpoint = MemeEndpoint.getSearchByTextMemeList(page: page, size: size, text: text)
let result = await networkservice.request(endpoint, dataType: BaseDTO<MemeWithPaginationResponseDTO>.self)

switch result {
case .success(let data):
guard let memeWithPaginationResponseDTO = data.data else { throw NetworkError.dataDecodingError }
return memeWithPaginationResponseDTO.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<MemeResponseDTO>.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public protocol MemeRepository {

func getRecommendMemes(size: Int) async throws -> [MemeDetail]
func getSearchKeywordMemeList(page: Int, size: Int, keyword: String) async throws -> MemeListWithPagination
func getSearchByTextMemeList(page: Int, size: Int, text: String) async throws -> MemeListWithPagination
func getMemeDetail(memeId: String) async throws -> MemeDetail
func bookmarkMeme(memeId: String) async throws
func deleteBookmarkMeme(memeId: String) async throws
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,19 @@ public class SearchKeywordUseCaseImpl: SearchKeywordUseCase {
try await repository.getSearchKeywordMemeList(page: page, size: size, keyword: keyword)
}
}

public protocol SearchByTextUseCase {
func execute(page: Int, size: Int, text: String) async throws -> MemeListWithPagination
}

public class SearchByTextUseCaseImpl: SearchByTextUseCase {
private let repository: MemeRepository

public init(repository: MemeRepository) {
self.repository = repository
}

public func execute(page: Int, size: Int, text: String) async throws -> MemeListWithPagination {
try await repository.getSearchByTextMemeList(page: page, size: size, text: text)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct MemeListWithPagination {
/// page 당 밈 개수
public let perPageOfMemes: Int
/// 현재 page
public let currentPage: Int
public var currentPage: Int

public init(
totalPages: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ public final class SearchResultRouter: Router, SearchResultRouting {
public var childRouters: [any Router] = []

let keyword: String

let text: String

// MARK: - Initializers

public init(_ navigationController: UINavigationController, keyword: String) {
public init(_ navigationController: UINavigationController, keyword: String, text: String) {
navigationController.isNavigationBarHidden = true
self.navigationController = navigationController
self.keyword = keyword
self.text = text
}

// MARK: - Methods
Expand All @@ -41,8 +43,10 @@ public final class SearchResultRouter: Router, SearchResultRouting {
self.pushView(
SearchResultView(viewModel: SearchResultViewModel(
keyword: keyword,
router: self,
text: text,
router: self,
searchKeywordUseCase: SearchKeywordUseCaseImpl(repository: repository),
searchByTextUseCase: SearchByTextUseCaseImpl(repository: repository),
copyImageUseCase: CopyImageUseCaseImpl(),
watchMemeUseCase: WatchMemeUseCaseImpl(repository: repository)
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,34 @@ import PopupView

public struct SearchResultView: View {
@ObservedObject var viewModel: SearchResultViewModel
@State var text: String

public init(viewModel: SearchResultViewModel) {
self.viewModel = viewModel
self.text = viewModel.state.text
}

public var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Button(action: {
viewModel.dispatch(type: .naviBackButtonTapped)
}) {
ResourceKitAsset.Icon.back.swiftUIImage
}

SearchBar(text: $text)
.onSubmit {
viewModel.dispatch(type: .search(text: text))
}
}
.padding(.horizontal, 20)
.padding(.vertical, 6)

Rectangle()
.fill(Color.Background.assistive)
.frame(maxWidth: .infinity)
.frame(height: 1)
.padding(.top, 51)

ScrollView {
VStack(alignment: .leading, spacing: 0) {
Expand All @@ -38,12 +54,6 @@ public struct SearchResultView: View {
}
}
}
.plainNavigationBar(
backHandler: { viewModel.dispatch(type: .naviBackButtonTapped) },
rightActionHandler: nil,
hasConfigureButton: false,
title: viewModel.state.keyword
)
.onAppear {
viewModel.dispatch(type: .viewWillAppear)
}
Expand All @@ -56,7 +66,7 @@ public struct SearchResultView: View {

private var memeListView: some View {
VStack(alignment: .leading, spacing: 0) {
Text("\(viewModel.state.memeList.count)개의 밈을 찾았어요")
Text("\(viewModel.state.memePagination.totalMemes)개의 밈을 찾았어요")
.font(Font.Body.Medium.medium)
.foregroundColor(Color.Text.primary)
.padding(.all, 20)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject {

public enum Action {
case viewWillAppear
case search(text: String)
case memeDetailTapped(meme: MemeDetail)
case memeCopyTapped(meme: MemeDetail)
case naviBackButtonTapped
Expand All @@ -35,6 +36,7 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject {

public struct State {
var keyword: String
var text: String
var memeList: [MemeDetail]
var memePagination: MemeListWithPagination.Pagination
var isActiveCopyPopup: Bool = false
Expand All @@ -47,21 +49,30 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject {
@Published public var state: State

private let searchKeywordUseCase: SearchKeywordUseCase
private let searchByTextUseCase: SearchByTextUseCase
private let copyImageUseCase: CopyImageUseCase
private let watchMemeUseCase: WatchMemeUseCase

// MARK: - Initializers

public init(
keyword: String,
text: String,
router: SearchResultRouting?,
searchKeywordUseCase: SearchKeywordUseCase,
searchByTextUseCase: SearchByTextUseCase,
copyImageUseCase: CopyImageUseCase,
watchMemeUseCase: WatchMemeUseCase
) {
self.router = router
self.state = State(keyword: keyword, memeList: [], memePagination: .default)
self.state = State(
keyword: keyword,
text: text,
memeList: [],
memePagination: .default
)
self.searchKeywordUseCase = searchKeywordUseCase
self.searchByTextUseCase = searchByTextUseCase
self.copyImageUseCase = copyImageUseCase
self.watchMemeUseCase = watchMemeUseCase
}
Expand All @@ -74,6 +85,8 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject {
switch type {
case .viewWillAppear:
await fetchData()
case .search(text: let text):
await fetchData(with: text)
case .memeDetailTapped(let meme):
router?.showMemeDetail(memeDetail: meme)
logSearch(event: .meme, keyword: state.keyword)
Expand All @@ -90,21 +103,45 @@ public final class SearchResultViewModel: ViewModelType, ObservableObject {
}

@MainActor
private func fetchData() async {
private func fetchData(with newText: String = "") async {
do {
guard state.memePagination.currentPage < state.memePagination.totalPages else { return }
if newText.isEmpty == false {
state.text = newText
state.keyword = ""
state.memeList = []
state.memePagination.currentPage = -1
}

guard state.memePagination.currentPage < state.memePagination.totalPages
else {
return
}
state.isLoading = true

let result = try await searchKeywordUseCase
.execute(
page: state.memePagination.currentPage + 1,
size: state.memePagination.perPageOfMemes,
keyword: state.keyword
)
if state.text.isEmpty == false {
let result = try await searchByTextUseCase
.execute(
page: state.memePagination.currentPage + 1,
size: state.memePagination.perPageOfMemes,
text: state.text
)

state.memeList += result.memeList
state.memePagination = result.pagination
state.isLoading = false

state.memeList += result.memeList
state.memePagination = result.pagination
state.isLoading = false
} else if state.keyword.isEmpty == false {
let result = try await searchKeywordUseCase
.execute(
page: state.memePagination.currentPage + 1,
size: state.memePagination.perPageOfMemes,
keyword: state.keyword
)

state.memeList += result.memeList
state.memePagination = result.pagination
state.isLoading = false
}

self.logSearch(
interaction: .scroll,
Expand Down
Loading
Loading