From 326a175f89f80d394da08f5826d8a424bcc15e8d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:40:11 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=ED=95=B4=EC=8B=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=95=88=EB=82=B4=20=EC=83=81=ED=83=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Resource/Localizable.xcstrings | 34 +++++++++++++++++++ .../Sources/Search/SearchFeature.swift | 16 +++++++++ .../Sources/Search/SearchView.swift | 18 ++++++++++ 3 files changed, 68 insertions(+) diff --git a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings index fba808af..0ff500fe 100644 --- a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings +++ b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings @@ -1595,6 +1595,40 @@ } } }, + "search_todo_number_instruction_message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Type like #123 to find that Todo directly." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "#123처럼 입력하면 해당 Todo를 바로 찾을 수 있습니다." + } + } + } + }, + "search_todo_number_instruction_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter a Todo number" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todo 번호를 입력해주세요" + } + } + } + }, "settings_account" : { "extractionState" : "manual", "localizations" : { diff --git a/Application/DevLogPresentation/Sources/Search/SearchFeature.swift b/Application/DevLogPresentation/Sources/Search/SearchFeature.swift index 2a26ebcb..633c2b7f 100644 --- a/Application/DevLogPresentation/Sources/Search/SearchFeature.swift +++ b/Application/DevLogPresentation/Sources/Search/SearchFeature.swift @@ -57,6 +57,10 @@ struct SearchFeature { var shouldShowMoreWebPages: Bool { !showAllWebPages && contentsLimit < webPages.count } + + var showsTodoNumberSearchInstruction: Bool { + SearchFeature.showsTodoNumberSearchInstruction(searchQuery) + } } enum Action: BindableAction, Equatable { @@ -112,6 +116,10 @@ struct SearchFeature { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) + } else if Self.showsTodoNumberSearchInstruction(trimmed) { + state.webPages = [] + state.todos = [] + return Self.cancelSearchEffect(isLoading: state.isLoading) } else { return .concatenate( Self.cancelSearchEffect(isLoading: state.isLoading), @@ -145,6 +153,10 @@ struct SearchFeature { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) + } else if Self.showsTodoNumberSearchInstruction(trimmed) { + state.webPages = [] + state.todos = [] + return Self.cancelSearchEffect(isLoading: state.isLoading) } else { return fetchEffect(trimmed, isLoading: state.isLoading) } @@ -277,6 +289,10 @@ private extension SearchFeature { query.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("#") } + static func showsTodoNumberSearchInstruction(_ query: String) -> Bool { + query.trimmingCharacters(in: .whitespacesAndNewlines) == "#" + } + static func fetchWebPageItems( query: String, searchesTodoOnly: Bool, diff --git a/Application/DevLogPresentation/Sources/Search/SearchView.swift b/Application/DevLogPresentation/Sources/Search/SearchView.swift index 80e4a637..532d8d96 100644 --- a/Application/DevLogPresentation/Sources/Search/SearchView.swift +++ b/Application/DevLogPresentation/Sources/Search/SearchView.swift @@ -62,6 +62,8 @@ struct SearchView: View { recentQueries } } + } else if store.showsTodoNumberSearchInstruction { + todoNumberSearchInstruction } else if store.isLoading { LoadingView() } else if store.webPages.isEmpty && store.todos.isEmpty { @@ -104,6 +106,22 @@ struct SearchView: View { .frame(maxWidth: .infinity) } + private var todoNumberSearchInstruction: some View { + VStack(spacing: 8) { + Spacer() + Text(String(localized: "search_todo_number_instruction_title")) + .font(.headline) + .foregroundStyle(Color(.label)) + Text(String(localized: "search_todo_number_instruction_message")) + .font(.subheadline) + .foregroundStyle(Color.gray) + .multilineTextAlignment(.center) + Spacer() + } + .padding(.horizontal, 24) + .frame(maxWidth: .infinity) + } + private var searchResults: some View { VStack(alignment: .leading, spacing: 16) { if !store.todos.isEmpty { From 5a771032a3d42630778fb16e92df0ec95cd19670 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:40:26 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20=ED=95=B4=EC=8B=9C=20=EB=8B=A8?= =?UTF-8?q?=EB=8F=85=20=EA=B2=80=EC=83=89=20=EC=95=88=EB=82=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Search/SearchFeatureTestDoubles.swift | 6 ++++- .../Tests/Search/SearchFeatureTests.swift | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift index 8925ca5b..ee38fbf9 100644 --- a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift +++ b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift @@ -24,6 +24,7 @@ struct SearchStoreTestAdapter { var recentQueries: [String] { Array(store.state.recentQueries) } var showAllTodos: Bool { store.state.showAllTodos } var showAllWebPages: Bool { store.state.showAllWebPages } + var showsTodoNumberSearchInstruction: Bool { store.state.showsTodoNumberSearchInstruction } var alert: AlertState? { store.state.alert } init( @@ -103,12 +104,15 @@ struct SearchStoreTestAdapter { if trimmed.isEmpty { $0.todos = [] $0.webPages = [] + } else if trimmed == "#" { + $0.todos = [] + $0.webPages = [] } } if wasLoading { await receiveEndLoading() } - if !trimmed.isEmpty { + if !trimmed.isEmpty && trimmed != "#" { await receiveBeginLoading() } } diff --git a/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift b/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift index 8bfb8507..be35fd7a 100644 --- a/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift +++ b/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift @@ -122,6 +122,30 @@ struct SearchFeatureTests { #expect(!adapter.isLoading) } + @Test("# 단독 검색어는 안내 상태로 전환하고 조회를 시작하지 않는다") + func 해시_단독_검색어는_안내_상태로_전환하고_조회를_시작하지_않는다() async { + let todo = TodoListItem(from: makeSearchTodo(id: "todo-1"))! + let webPage = WebPageItem(from: makeSearchWebPage(urlString: "https://swift.org")) + let todoSpy = SearchFetchTodosUseCaseSpy() + let webSpy = SearchFetchWebPagesUseCaseSpy() + let adapter = SearchStoreTestAdapter( + initialTodos: [todo], + initialWebPages: [webPage], + isLoading: true, + fetchWebPagesUseCase: webSpy, + fetchTodosUseCase: todoSpy + ) + + await adapter.setSearchQuery("#") + + #expect(adapter.showsTodoNumberSearchInstruction) + #expect(adapter.todos.isEmpty) + #expect(adapter.webPages.isEmpty) + #expect(!adapter.isLoading) + #expect(todoSpy.queries.isEmpty) + #expect(webSpy.queries.isEmpty) + } + @Test("# 검색어는 WebPage 조회를 생략하고 Todo만 반영한다") func 해시태그_검색어는_WebPage_조회를_생략하고_Todo만_반영한다() async { let todo = makeSearchTodo(id: "todo-1", title: "Issue") From f97a9894c4bec2cfa7d019a0a54b32f301d5d370 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:45:59 +0900 Subject: [PATCH 3/5] =?UTF-8?q?chore:=20=EA=B2=80=EC=83=89=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8D=94=EB=B8=94=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/Search/SearchFeatureTestDoubles.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift index ee38fbf9..2ded4bb7 100644 --- a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift +++ b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift @@ -101,10 +101,7 @@ struct SearchStoreTestAdapter { $0.searchQuery = query $0.showAllTodos = false $0.showAllWebPages = false - if trimmed.isEmpty { - $0.todos = [] - $0.webPages = [] - } else if trimmed == "#" { + if trimmed.isEmpty || $0.showsTodoNumberSearchInstruction { $0.todos = [] $0.webPages = [] } @@ -112,7 +109,7 @@ struct SearchStoreTestAdapter { if wasLoading { await receiveEndLoading() } - if !trimmed.isEmpty && trimmed != "#" { + if !trimmed.isEmpty && !store.state.showsTodoNumberSearchInstruction { await receiveBeginLoading() } } From 4a18e4621f50ebf75139e93e16282e3d0acfc062 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:54:38 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=ED=95=B4=EC=8B=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=83=81=ED=83=9C=20=EC=9D=B4=EB=A6=84=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevLogApp/Sources/Resource/Localizable.xcstrings | 4 ++-- .../Sources/Search/SearchFeature.swift | 10 +++++----- .../DevLogPresentation/Sources/Search/SearchView.swift | 10 +++++----- .../Tests/Search/SearchFeatureTestDoubles.swift | 6 +++--- .../Tests/Search/SearchFeatureTests.swift | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings index 0ff500fe..6a0c1aee 100644 --- a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings +++ b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings @@ -1595,7 +1595,7 @@ } } }, - "search_todo_number_instruction_message" : { + "search_hash_guide_message" : { "extractionState" : "manual", "localizations" : { "en" : { @@ -1612,7 +1612,7 @@ } } }, - "search_todo_number_instruction_title" : { + "search_hash_guide_title" : { "extractionState" : "manual", "localizations" : { "en" : { diff --git a/Application/DevLogPresentation/Sources/Search/SearchFeature.swift b/Application/DevLogPresentation/Sources/Search/SearchFeature.swift index 633c2b7f..2e6d156b 100644 --- a/Application/DevLogPresentation/Sources/Search/SearchFeature.swift +++ b/Application/DevLogPresentation/Sources/Search/SearchFeature.swift @@ -58,8 +58,8 @@ struct SearchFeature { !showAllWebPages && contentsLimit < webPages.count } - var showsTodoNumberSearchInstruction: Bool { - SearchFeature.showsTodoNumberSearchInstruction(searchQuery) + var isHashOnlyQuery: Bool { + SearchFeature.isHashOnlyQuery(searchQuery) } } @@ -116,7 +116,7 @@ struct SearchFeature { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) - } else if Self.showsTodoNumberSearchInstruction(trimmed) { + } else if Self.isHashOnlyQuery(trimmed) { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) @@ -153,7 +153,7 @@ struct SearchFeature { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) - } else if Self.showsTodoNumberSearchInstruction(trimmed) { + } else if Self.isHashOnlyQuery(trimmed) { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) @@ -289,7 +289,7 @@ private extension SearchFeature { query.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("#") } - static func showsTodoNumberSearchInstruction(_ query: String) -> Bool { + static func isHashOnlyQuery(_ query: String) -> Bool { query.trimmingCharacters(in: .whitespacesAndNewlines) == "#" } diff --git a/Application/DevLogPresentation/Sources/Search/SearchView.swift b/Application/DevLogPresentation/Sources/Search/SearchView.swift index 532d8d96..4511d392 100644 --- a/Application/DevLogPresentation/Sources/Search/SearchView.swift +++ b/Application/DevLogPresentation/Sources/Search/SearchView.swift @@ -62,8 +62,8 @@ struct SearchView: View { recentQueries } } - } else if store.showsTodoNumberSearchInstruction { - todoNumberSearchInstruction + } else if store.isHashOnlyQuery { + hashGuide } else if store.isLoading { LoadingView() } else if store.webPages.isEmpty && store.todos.isEmpty { @@ -106,13 +106,13 @@ struct SearchView: View { .frame(maxWidth: .infinity) } - private var todoNumberSearchInstruction: some View { + private var hashGuide: some View { VStack(spacing: 8) { Spacer() - Text(String(localized: "search_todo_number_instruction_title")) + Text(String(localized: "search_hash_guide_title")) .font(.headline) .foregroundStyle(Color(.label)) - Text(String(localized: "search_todo_number_instruction_message")) + Text(String(localized: "search_hash_guide_message")) .font(.subheadline) .foregroundStyle(Color.gray) .multilineTextAlignment(.center) diff --git a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift index 2ded4bb7..6bd8dec0 100644 --- a/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift +++ b/Application/DevLogPresentation/Tests/Search/SearchFeatureTestDoubles.swift @@ -24,7 +24,7 @@ struct SearchStoreTestAdapter { var recentQueries: [String] { Array(store.state.recentQueries) } var showAllTodos: Bool { store.state.showAllTodos } var showAllWebPages: Bool { store.state.showAllWebPages } - var showsTodoNumberSearchInstruction: Bool { store.state.showsTodoNumberSearchInstruction } + var isHashOnlyQuery: Bool { store.state.isHashOnlyQuery } var alert: AlertState? { store.state.alert } init( @@ -101,7 +101,7 @@ struct SearchStoreTestAdapter { $0.searchQuery = query $0.showAllTodos = false $0.showAllWebPages = false - if trimmed.isEmpty || $0.showsTodoNumberSearchInstruction { + if trimmed.isEmpty || $0.isHashOnlyQuery { $0.todos = [] $0.webPages = [] } @@ -109,7 +109,7 @@ struct SearchStoreTestAdapter { if wasLoading { await receiveEndLoading() } - if !trimmed.isEmpty && !store.state.showsTodoNumberSearchInstruction { + if !trimmed.isEmpty && !store.state.isHashOnlyQuery { await receiveBeginLoading() } } diff --git a/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift b/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift index be35fd7a..72caa2ef 100644 --- a/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift +++ b/Application/DevLogPresentation/Tests/Search/SearchFeatureTests.swift @@ -138,7 +138,7 @@ struct SearchFeatureTests { await adapter.setSearchQuery("#") - #expect(adapter.showsTodoNumberSearchInstruction) + #expect(adapter.isHashOnlyQuery) #expect(adapter.todos.isEmpty) #expect(adapter.webPages.isEmpty) #expect(!adapter.isLoading) From 4b94bcc0a5bd0d4d3f62fd5726bf519258b21aeb Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:03:30 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=ED=97=AC=ED=8D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Search/SearchFeature.swift | 47 +++---------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Search/SearchFeature.swift b/Application/DevLogPresentation/Sources/Search/SearchFeature.swift index 2e6d156b..1cbbc1af 100644 --- a/Application/DevLogPresentation/Sources/Search/SearchFeature.swift +++ b/Application/DevLogPresentation/Sources/Search/SearchFeature.swift @@ -59,7 +59,7 @@ struct SearchFeature { } var isHashOnlyQuery: Bool { - SearchFeature.isHashOnlyQuery(searchQuery) + searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) == "#" } } @@ -112,11 +112,7 @@ struct SearchFeature { state.showAllTodos = false state.showAllWebPages = false let trimmed = state.searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) - if trimmed.isEmpty { - state.webPages = [] - state.todos = [] - return Self.cancelSearchEffect(isLoading: state.isLoading) - } else if Self.isHashOnlyQuery(trimmed) { + if trimmed.isEmpty || trimmed == "#" { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) @@ -149,11 +145,7 @@ struct SearchFeature { return saveRecentQueriesEffect([]) case .store(.applySearchQuery(let query)): let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) - if trimmed.isEmpty { - state.webPages = [] - state.todos = [] - return Self.cancelSearchEffect(isLoading: state.isLoading) - } else if Self.isHashOnlyQuery(trimmed) { + if trimmed.isEmpty || trimmed == "#" { state.webPages = [] state.todos = [] return Self.cancelSearchEffect(isLoading: state.isLoading) @@ -244,20 +236,16 @@ private extension SearchFeature { } func fetchEffect(_ query: String, isLoading: Bool) -> Effect { - let searchesTodoOnly = Self.searchesTodoOnly(query) + let skipsWebPages = query.hasPrefix("#") return .run { [fetchTodosUseCase, fetchWebPagesUseCase] send in do { async let todos = fetchTodosUseCase.execute(TodoQuery(keyword: query), cursor: nil) - async let webPageItems = Self.fetchWebPageItems( - query: query, - searchesTodoOnly: searchesTodoOnly, - fetchWebPagesUseCase: fetchWebPagesUseCase - ) + let webPages = skipsWebPages ? [] : try await fetchWebPagesUseCase.execute(query) let todoItems = try await todos.items.compactMap { TodoListItem(from: $0) } - let resolvedWebPageItems = try await webPageItems + let webPageItems = webPages.map { WebPageItem(from: $0) } await send(.store(.fetchTodos(todoItems))) - await send(.store(.fetchWebPage(resolvedWebPageItems))) + await send(.store(.fetchWebPage(webPageItems))) if isLoading { await send(.loading(.end(target: .default, mode: .immediate))) } @@ -285,27 +273,6 @@ private extension SearchFeature { } } - static func searchesTodoOnly(_ query: String) -> Bool { - query.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("#") - } - - static func isHashOnlyQuery(_ query: String) -> Bool { - query.trimmingCharacters(in: .whitespacesAndNewlines) == "#" - } - - static func fetchWebPageItems( - query: String, - searchesTodoOnly: Bool, - fetchWebPagesUseCase: FetchWebPagesUseCase - ) async throws -> [WebPageItem] { - if searchesTodoOnly { - return [] - } - - let webPages = try await fetchWebPagesUseCase.execute(query) - return webPages.map { WebPageItem(from: $0) } - } - static func alertState() -> AlertState { AlertState { TextState(String(localized: "common_error_title"))