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
55 changes: 34 additions & 21 deletions Application/DevLogInfra/Sources/Service/TodoServiceImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,9 @@ final class TodoServiceImpl: TodoService {
let snapshot = try await firestoreQuery.getDocuments()
let todos = snapshot.documents.compactMap { makeResponse(from: $0) }

let todoNumber = searchedTodoNumber(from: trimmedKeyword)
let numberKeyword = TodoResponse.normalizedNumberKeyword(from: trimmedKeyword) ?? 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, numberKeyword: numberKeyword)
}
Comment thread
opficdev marked this conversation as resolved.

return TodoPageResponse(items: filtered, nextCursor: nil)
Expand Down Expand Up @@ -330,6 +324,38 @@ final class TodoServiceImpl: TodoService {
}
}

extension TodoResponse {
func matchesSearchKeyword(_ keyword: String, numberKeyword: String? = nil) -> Bool {
let resolvedNumberKeyword = numberKeyword ?? Self.normalizedNumberKeyword(from: keyword) ?? keyword

if keyword.hasPrefix("#"),
1 < keyword.count,
"#\(number)".localizedCaseInsensitiveContains(resolvedNumberKeyword) {
return true
}

return title.localizedCaseInsensitiveContains(keyword)
|| content.localizedCaseInsensitiveContains(keyword)
|| tags.contains { $0.localizedCaseInsensitiveContains(keyword) }
}

static 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)"
}
}
Comment thread
opficdev marked this conversation as resolved.

private extension TodoServiceImpl {
private static func record(_ error: Error, code: CrashlyticsError.Code) {
FirebaseCrashlyticsHelper.record(
Expand Down Expand Up @@ -527,19 +553,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// 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)
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 번호로 매칭하지 않는다")
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")
)
}
}
Loading