From 8bfab0f8e12a84f82feeb3c37b1e85dab58125a8 Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 6 Apr 2026 11:14:30 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EA=B2=80=EC=83=89=20=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=9D=B4=EC=8A=88=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/firestore.index.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Firebase/firestore.index.json b/Firebase/firestore.index.json index 9162f6c2..914b6b4d 100644 --- a/Firebase/firestore.index.json +++ b/Firebase/firestore.index.json @@ -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", From f7e9c745575238d627168f59afe88ac58310d318 Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 6 Apr 2026 11:21:46 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EA=B2=80=EC=83=89=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=96=B4=EA=B0=80=20=EC=97=86=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20LoadingView=EA=B0=80=20=EA=B0=84=ED=97=90=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=9C=A8=EB=8A=94=20=ED=98=84=EC=83=81=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Presentation/ViewModel/SearchViewModel.swift | 1 + DevLog/UI/Search/SearchView.swift | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DevLog/Presentation/ViewModel/SearchViewModel.swift b/DevLog/Presentation/ViewModel/SearchViewModel.swift index 0a8ab7a5..7d790cd9 100644 --- a/DevLog/Presentation/ViewModel/SearchViewModel.swift +++ b/DevLog/Presentation/ViewModel/SearchViewModel.swift @@ -107,6 +107,7 @@ final class SearchViewModel: Store { cancelDebounce() state.webPages = [] state.todos = [] + state.isLoading = false } else { state.isLoading = true scheduleDebouncedQuery(query) diff --git a/DevLog/UI/Search/SearchView.swift b/DevLog/UI/Search/SearchView.swift index dec3802b..2c63c3c5 100644 --- a/DevLog/UI/Search/SearchView.swift +++ b/DevLog/UI/Search/SearchView.swift @@ -61,9 +61,7 @@ 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 { @@ -71,6 +69,8 @@ struct SearchView: View { recentQueries } } + } else if viewModel.state.isLoading { + LoadingView() } else if viewModel.state.webPages.isEmpty && viewModel.state.todos.isEmpty { emptySearchResult } else { From db5a528a45b85ada69b446390ab363625ceb8b84 Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 6 Apr 2026 12:04:56 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20'#=EC=88=AB=EC=9E=90'=20=EB=A5=BC?= =?UTF-8?q?=20=EB=B6=99=EC=9D=B4=EB=A9=B4=20Todo=EC=9D=98=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EB=A1=9C=EB=A7=8C=20=EA=B2=80=EC=83=89=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Infra/Service/TodoService.swift | 20 +++++++++++++- .../ViewModel/SearchViewModel.swift | 26 ++++++++++++++++--- DevLog/Resource/Localizable.xcstrings | 10 +++---- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/DevLog/Infra/Service/TodoService.swift b/DevLog/Infra/Service/TodoService.swift index f50b562e..acb4536a 100644 --- a/DevLog/Infra/Service/TodoService.swift +++ b/DevLog/Infra/Service/TodoService.swift @@ -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) } } @@ -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 diff --git a/DevLog/Presentation/ViewModel/SearchViewModel.swift b/DevLog/Presentation/ViewModel/SearchViewModel.swift index 7d790cd9..6bfc606b 100644 --- a/DevLog/Presentation/ViewModel/SearchViewModel.swift +++ b/DevLog/Presentation/ViewModel/SearchViewModel.swift @@ -137,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)) } @@ -178,6 +182,22 @@ private extension SearchViewModel { searchDebounceTask = nil } + func searchesTodoOnly(_ query: String) -> Bool { + query.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("#") + } + + 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) { updateRecentSearchQueriesUseCase.execute(Array(queries)) } diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index 5e533343..4a057543 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -1495,13 +1495,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Search" + "value" : "Search (e.g. #123)" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "검색" + "value" : "검색 (예: #123)" } } } @@ -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)" } } } @@ -3367,4 +3367,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +}