From c1c3027359c739ed2008aedecc257048455dec33 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:31:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20SearchView=20#=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=EC=9D=84=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Service/TodoServiceImpl.swift | 36 ++++++-------- .../Service/TodoSearchMatchingTests.swift | 47 +++++++++++++++++++ 2 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift diff --git a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift index 9fea958d..d462af99 100644 --- a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift @@ -167,15 +167,8 @@ final class TodoServiceImpl: 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 - if let todoNumber, todo.number == todoNumber { - return true - } - - return todo.title.localizedCaseInsensitiveContains(trimmedKeyword) - || todo.content.localizedCaseInsensitiveContains(trimmedKeyword) - || todo.tags.contains { $0.localizedCaseInsensitiveContains(trimmedKeyword) } + todo.matchesSearchKeyword(trimmedKeyword) } return TodoPageResponse(items: filtered, nextCursor: nil) @@ -330,6 +323,20 @@ final class TodoServiceImpl: TodoService { } } +extension TodoResponse { + func matchesSearchKeyword(_ keyword: String) -> Bool { + if keyword.hasPrefix("#"), + 1 < keyword.count, + "#\(number)".localizedCaseInsensitiveContains(keyword) { + return true + } + + return title.localizedCaseInsensitiveContains(keyword) + || content.localizedCaseInsensitiveContains(keyword) + || tags.contains { $0.localizedCaseInsensitiveContains(keyword) } + } +} + private extension TodoServiceImpl { private static func record(_ error: Error, code: CrashlyticsError.Code) { FirebaseCrashlyticsHelper.record( @@ -527,19 +534,6 @@ private extension TodoServiceImpl { ) } - 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/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift b/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift new file mode 100644 index 00000000..86e7f6b5 --- /dev/null +++ b/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift @@ -0,0 +1,47 @@ +// +// TodoSearchMatchingTests.swift +// DevLogInfraTests +// +// Created by opfic on 6/26/26. +// + +import Foundation +import Testing +import DevLogData +@testable import DevLogInfra + +struct TodoSearchMatchingTests { + @Test("#숫자 검색어는 Todo 번호를 문자열 기반으로 부분 검색한다") + func 해시_숫자_검색어는_Todo_번호를_문자열_기반으로_부분_검색한다() { + let todo = makeTodo(number: 123) + + #expect(todo.matchesSearchKeyword("#1")) + #expect(todo.matchesSearchKeyword("#12")) + } + + @Test("# 단독 검색어는 Todo 번호로 매칭하지 않는다") + func 해시_단독_검색어는_Todo_번호로_매칭하지_않는다() { + let todo = makeTodo(number: 123) + + #expect(!todo.matchesSearchKeyword("#")) + } + + private func makeTodo(number: Int) -> TodoResponse { + TodoResponse( + id: "todo-id", + isPinned: false, + isCompleted: false, + isChecked: false, + number: number, + title: "title", + content: "content", + createdAt: .now, + updatedAt: .now, + completedAt: nil, + deletedAt: nil, + dueDate: nil, + tags: [], + category: .raw("feature") + ) + } +} From 7332d4a9020a56bbb58d35086b7b80fb281d9dad Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:32:12 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20SearchView=20#=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=EC=96=B4=EC=9D=98=20=EC=84=A0=ED=96=89=20?= =?UTF-8?q?0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Service/TodoServiceImpl.swift | 20 ++++++++++++++++++- .../Service/TodoSearchMatchingTests.swift | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift index d462af99..12d39267 100644 --- a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift @@ -325,9 +325,11 @@ final class TodoServiceImpl: TodoService { extension TodoResponse { func matchesSearchKeyword(_ keyword: String) -> Bool { + let numberKeyword = normalizedNumberKeyword(from: keyword) ?? keyword + if keyword.hasPrefix("#"), 1 < keyword.count, - "#\(number)".localizedCaseInsensitiveContains(keyword) { + "#\(number)".localizedCaseInsensitiveContains(numberKeyword) { return true } @@ -335,6 +337,22 @@ extension TodoResponse { || content.localizedCaseInsensitiveContains(keyword) || tags.contains { $0.localizedCaseInsensitiveContains(keyword) } } + + private func normalizedNumberKeyword(from keyword: String) -> String? { + guard keyword.hasPrefix("#") else { + return nil + } + + let digits = keyword.dropFirst() + guard !digits.isEmpty, digits.allSatisfy(\.isNumber) else { + return nil + } + + let normalizedDigits = digits.drop(while: { $0 == "0" }) + let numberText = normalizedDigits.isEmpty ? "0" : String(normalizedDigits) + + return "#\(numberText)" + } } private extension TodoServiceImpl { diff --git a/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift b/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift index 86e7f6b5..6fe17798 100644 --- a/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift +++ b/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift @@ -17,6 +17,7 @@ struct TodoSearchMatchingTests { #expect(todo.matchesSearchKeyword("#1")) #expect(todo.matchesSearchKeyword("#12")) + #expect(todo.matchesSearchKeyword("#0001")) } @Test("# 단독 검색어는 Todo 번호로 매칭하지 않는다") From ec0b23b63ff3a730199fc85920f7f6e1906661c4 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:43:02 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20Todo=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=95=EA=B7=9C=ED=99=94=20?= =?UTF-8?q?=EB=B0=98=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogInfra/Sources/Service/TodoServiceImpl.swift | 11 ++++++----- .../Tests/Service/TodoSearchMatchingTests.swift | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift index 12d39267..9f32e727 100644 --- a/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift +++ b/Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift @@ -167,8 +167,9 @@ final class TodoServiceImpl: TodoService { let snapshot = try await firestoreQuery.getDocuments() let todos = snapshot.documents.compactMap { makeResponse(from: $0) } + let numberKeyword = TodoResponse.normalizedNumberKeyword(from: trimmedKeyword) ?? trimmedKeyword let filtered = todos.filter { todo in - todo.matchesSearchKeyword(trimmedKeyword) + todo.matchesSearchKeyword(trimmedKeyword, numberKeyword: numberKeyword) } return TodoPageResponse(items: filtered, nextCursor: nil) @@ -324,12 +325,12 @@ final class TodoServiceImpl: TodoService { } extension TodoResponse { - func matchesSearchKeyword(_ keyword: String) -> Bool { - let numberKeyword = normalizedNumberKeyword(from: keyword) ?? keyword + func matchesSearchKeyword(_ keyword: String, numberKeyword: String? = nil) -> Bool { + let resolvedNumberKeyword = numberKeyword ?? Self.normalizedNumberKeyword(from: keyword) ?? keyword if keyword.hasPrefix("#"), 1 < keyword.count, - "#\(number)".localizedCaseInsensitiveContains(numberKeyword) { + "#\(number)".localizedCaseInsensitiveContains(resolvedNumberKeyword) { return true } @@ -338,7 +339,7 @@ extension TodoResponse { || tags.contains { $0.localizedCaseInsensitiveContains(keyword) } } - private func normalizedNumberKeyword(from keyword: String) -> String? { + static func normalizedNumberKeyword(from keyword: String) -> String? { guard keyword.hasPrefix("#") else { return nil } diff --git a/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift b/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift index 6fe17798..90fd34f1 100644 --- a/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift +++ b/Application/DevLogInfra/Tests/Service/TodoSearchMatchingTests.swift @@ -14,10 +14,12 @@ struct TodoSearchMatchingTests { @Test("#숫자 검색어는 Todo 번호를 문자열 기반으로 부분 검색한다") func 해시_숫자_검색어는_Todo_번호를_문자열_기반으로_부분_검색한다() { let todo = makeTodo(number: 123) + let numberKeyword = TodoResponse.normalizedNumberKeyword(from: "#0001") #expect(todo.matchesSearchKeyword("#1")) #expect(todo.matchesSearchKeyword("#12")) #expect(todo.matchesSearchKeyword("#0001")) + #expect(todo.matchesSearchKeyword("#0001", numberKeyword: numberKeyword)) } @Test("# 단독 검색어는 Todo 번호로 매칭하지 않는다")