Skip to content
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
20 changes: 19 additions & 1 deletion DevLog/Infra/Service/TodoService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,13 @@ final class TodoService {
let snapshot = try await firestoreQuery.getDocuments()
let todos = snapshot.documents.compactMap { makeResponse(from: $0) }

let todoNumber = searchedTodoNumber(from: trimmedKeyword)
let filtered = todos.filter { todo in
todo.title.localizedCaseInsensitiveContains(trimmedKeyword)
if let todoNumber, todo.number == todoNumber {
return true
}

return todo.title.localizedCaseInsensitiveContains(trimmedKeyword)
|| todo.content.localizedCaseInsensitiveContains(trimmedKeyword)
|| todo.tags.contains { $0.localizedCaseInsensitiveContains(trimmedKeyword) }
}
Expand Down Expand Up @@ -483,6 +488,19 @@ private extension TodoService {
)
}

func searchedTodoNumber(from keyword: String) -> Int? {
guard keyword.hasPrefix("#") else {
return nil
}

let numberText = String(keyword.dropFirst())
guard !numberText.isEmpty, numberText.allSatisfy(\.isNumber) else {
return nil
}

return Int(numberText)
}

enum TodoFieldKey: String {
case id
case isPinned
Expand Down
27 changes: 24 additions & 3 deletions DevLog/Presentation/ViewModel/SearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ final class SearchViewModel: Store {
cancelDebounce()
state.webPages = []
state.todos = []
state.isLoading = false
} else {
state.isLoading = true
scheduleDebouncedQuery(query)
Expand Down Expand Up @@ -136,12 +137,16 @@ final class SearchViewModel: Store {
do {
send(.setLoading(true))
defer { send(.setLoading(false)) }
let searchesTodoOnly = searchesTodoOnly(query)
async let todos = fetchTodosUseCase.execute(TodoQuery(keyword: query), cursor: nil)
async let webPages = fetchWebPagesUseCase.execute(query)
async let webPageItems = fetchWebPageItems(
query: query,
searchesTodoOnly: searchesTodoOnly
)
let todoItems = try await todos.items.compactMap { TodoListItem(from: $0) }
let webPageItems = try await webPages.map { WebPageItem(from: $0) }
let resolvedWebPageItems = try await webPageItems
send(.fetchTodos(todoItems))
send(.fetchWebPage(webPageItems))
send(.fetchWebPage(resolvedWebPageItems))
} catch {
send(.setAlert(true))
}
Expand Down Expand Up @@ -177,6 +182,22 @@ private extension SearchViewModel {
searchDebounceTask = nil
}

func searchesTodoOnly(_ query: String) -> Bool {
query.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("#")
}
Comment on lines +185 to +187
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

searchesTodoOnly 메서드가 #으로 시작하는 모든 검색어에 대해 true를 반환하고 있습니다. 이로 인해 #swift와 같은 일반적인 해시태그 검색 시에도 웹 페이지 검색 결과가 차단되는 부작용이 발생할 수 있습니다. Todo 레퍼런스 번호 검색(예: #123)인 경우에만 웹 페이지 검색을 건너뛰도록 숫자인지 확인하는 로직을 추가하는 것이 좋습니다.

    func searchesTodoOnly(_ query: String) -> Bool {
        let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
        guard trimmed.hasPrefix("#") else {
            return false
        }

        let numberText = trimmed.dropFirst()
        return !numberText.isEmpty && numberText.allSatisfy(\.isNumber)
    }


func fetchWebPageItems(
query: String,
searchesTodoOnly: Bool
) async throws -> [WebPageItem] {
if searchesTodoOnly {
return []
}

let webPages = try await fetchWebPagesUseCase.execute(query)
return webPages.map { WebPageItem(from: $0) }
}

func saveRecentQueries(_ queries: OrderedSet<String>) {
updateRecentSearchQueriesUseCase.execute(Array(queries))
}
Expand Down
10 changes: 5 additions & 5 deletions DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1495,13 +1495,13 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Search"
"value" : "Search (e.g. #123)"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "검색"
"value" : "검색 (예: #123)"
}
}
}
Expand Down Expand Up @@ -2950,13 +2950,13 @@
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Search %1$@"
"value" : "Search %1$@ (e.g. #123)"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ 검색"
"value" : "%1$@ 검색 (예: #123)"
}
}
}
Expand Down Expand Up @@ -3367,4 +3367,4 @@
}
},
"version" : "1.0"
}
}
6 changes: 3 additions & 3 deletions DevLog/UI/Search/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,16 @@ struct SearchView: View {
@ViewBuilder
private var searchableContent: some View {
Group {
if viewModel.state.isLoading {
LoadingView()
} else if viewModel.state.searchQuery.isEmpty {
if viewModel.state.searchQuery.isEmpty {
if viewModel.state.recentQueries.isEmpty {
searchInstruction
} else {
ScrollView {
recentQueries
}
}
} else if viewModel.state.isLoading {
LoadingView()
} else if viewModel.state.webPages.isEmpty && viewModel.state.todos.isEmpty {
emptySearchResult
} else {
Expand Down
18 changes: 18 additions & 0 deletions Firebase/firestore.index.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@
}
]
},
{
"collectionGroup": "todoLists",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "deletedAt",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
},
{
"fieldPath": "__name__",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "todoLists",
"queryScope": "COLLECTION",
Expand Down