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
34 changes: 34 additions & 0 deletions Application/DevLogApp/Sources/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,40 @@
}
}
},
"search_hash_guide_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_hash_guide_title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enter a Todo number"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "Todo 번호를 입력해주세요"
}
}
}
},
"settings_account" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
37 changes: 10 additions & 27 deletions Application/DevLogPresentation/Sources/Search/SearchFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ struct SearchFeature {
var shouldShowMoreWebPages: Bool {
!showAllWebPages && contentsLimit < webPages.count
}

var isHashOnlyQuery: Bool {
searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) == "#"
}
}

enum Action: BindableAction, Equatable {
Expand Down Expand Up @@ -108,7 +112,7 @@ struct SearchFeature {
state.showAllTodos = false
state.showAllWebPages = false
let trimmed = state.searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
if trimmed.isEmpty || trimmed == "#" {
state.webPages = []
state.todos = []
return Self.cancelSearchEffect(isLoading: state.isLoading)
Expand Down Expand Up @@ -141,7 +145,7 @@ struct SearchFeature {
return saveRecentQueriesEffect([])
case .store(.applySearchQuery(let query)):
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
if trimmed.isEmpty || trimmed == "#" {
state.webPages = []
state.todos = []
return Self.cancelSearchEffect(isLoading: state.isLoading)
Expand Down Expand Up @@ -232,20 +236,16 @@ private extension SearchFeature {
}

func fetchEffect(_ query: String, isLoading: Bool) -> Effect<Action> {
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)))
}
Expand Down Expand Up @@ -273,23 +273,6 @@ private extension SearchFeature {
}
}

static func searchesTodoOnly(_ query: String) -> Bool {
query.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("#")
}

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<Never> {
AlertState {
TextState(String(localized: "common_error_title"))
Expand Down
18 changes: 18 additions & 0 deletions Application/DevLogPresentation/Sources/Search/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ struct SearchView: View {
recentQueries
}
}
} else if store.isHashOnlyQuery {
hashGuide
} else if store.isLoading {
LoadingView()
} else if store.webPages.isEmpty && store.todos.isEmpty {
Expand Down Expand Up @@ -104,6 +106,22 @@ struct SearchView: View {
.frame(maxWidth: .infinity)
}

private var hashGuide: some View {
VStack(spacing: 8) {
Spacer()
Text(String(localized: "search_hash_guide_title"))
.font(.headline)
.foregroundStyle(Color(.label))
Text(String(localized: "search_hash_guide_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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 isHashOnlyQuery: Bool { store.state.isHashOnlyQuery }
var alert: AlertState<Never>? { store.state.alert }

init(
Expand Down Expand Up @@ -100,15 +101,15 @@ struct SearchStoreTestAdapter {
$0.searchQuery = query
$0.showAllTodos = false
$0.showAllWebPages = false
if trimmed.isEmpty {
if trimmed.isEmpty || $0.isHashOnlyQuery {
$0.todos = []
$0.webPages = []
}
}
if wasLoading {
await receiveEndLoading()
}
if !trimmed.isEmpty {
if !trimmed.isEmpty && !store.state.isHashOnlyQuery {
await receiveBeginLoading()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.isHashOnlyQuery)
#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")
Expand Down
Loading