Skip to content

Commit a1a1d88

Browse files
authored
Merge pull request #164 from SOPT-all/refactor/#163-SearchViewRefactorMVI
Refactor/#163 search view refactor mvi
2 parents 0f10147 + 1778c10 commit a1a1d88

File tree

9 files changed

+413
-277
lines changed

9 files changed

+413
-277
lines changed

Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ struct Home: View {
8383
}
8484
}
8585
}
86-
} // ZStack 닫기
86+
}
8787
.navigationBarHidden(true)
8888
.task {
8989
isBottomSheetPresented = true

Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import Foundation
99

10-
struct SearchResult: Identifiable {
10+
struct SearchResult: Identifiable, Equatable {
1111
let id = UUID()
1212
let title: String
1313
let address: String
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// SearchModel.swift
3+
// Spoony-iOS
4+
//
5+
// Created by 이지훈 on 1/24/25.
6+
//
7+
8+
import Foundation
9+
10+
struct SearchModel {
11+
var searchText: String = ""
12+
var recentSearches: [String] = UserManager.shared.recentSearches ?? []
13+
var isFirstAppear: Bool = true
14+
var isSearchFocused: Bool = false
15+
}

Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchState.swift

+29-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,34 @@
77

88
import Foundation
99

10-
enum SearchState {
10+
enum SearchState: Equatable {
1111
case empty
12-
case typing
13-
case searched
12+
case typing(searchText: String)
13+
case loading
14+
case success(results: [SearchResult])
15+
case error(message: String)
16+
17+
static func == (lhs: SearchState, rhs: SearchState) -> Bool {
18+
switch (lhs, rhs) {
19+
case (.empty, .empty):
20+
return true
21+
case let (.typing(lhsText), .typing(rhsText)):
22+
return lhsText == rhsText
23+
case (.loading, .loading):
24+
return true
25+
case let (.success(lhsResults), .success(rhsResults)):
26+
return lhsResults == rhsResults
27+
case let (.error(lhsMessage), .error(rhsMessage)):
28+
return lhsMessage == rhsMessage
29+
default:
30+
return false
31+
}
32+
}
33+
34+
var results: [SearchResult] {
35+
if case let .success(results) = self {
36+
return results
37+
}
38+
return []
39+
}
1440
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//
2+
// SearchStore.swift
3+
// Spoony-iOS
4+
//
5+
// Created by 이지훈 on 1/24/25.
6+
//
7+
8+
import SwiftUI
9+
10+
@MainActor
11+
final class SearchStore: ObservableObject {
12+
@Published private(set) var state: SearchState = .empty
13+
@Published private(set) var model: SearchModel
14+
15+
private let searchService: SearchService
16+
private var navigationManager: NavigationManager
17+
18+
init(navigationManager: NavigationManager) {
19+
self.model = SearchModel()
20+
self.searchService = SearchService()
21+
self.navigationManager = navigationManager
22+
}
23+
24+
func dispatch(_ intent: SearchIntent) {
25+
switch intent {
26+
case .updateSearchText(let newText):
27+
handleSearchTextUpdate(newText)
28+
case .search:
29+
handleSearch()
30+
case .clearSearch:
31+
handleClearSearch()
32+
case .removeRecentSearch(let search):
33+
handleRemoveRecentSearch(search)
34+
case .clearAllRecentSearches:
35+
handleClearAllRecentSearches()
36+
case .selectLocation(let result):
37+
handleLocationSelection(result)
38+
case .setFirstAppear(let isFirst):
39+
model.isFirstAppear = isFirst
40+
}
41+
}
42+
43+
private func handleSearchTextUpdate(_ newText: String) {
44+
model.searchText = newText
45+
let normalizedText = newText.trimmingCharacters(in: .whitespacesAndNewlines)
46+
47+
if normalizedText.isEmpty {
48+
state = .empty
49+
} else {
50+
51+
state = .typing(searchText: normalizedText)
52+
}
53+
}
54+
55+
private func handleSearch() {
56+
guard !model.searchText.isEmpty else { return }
57+
58+
let normalizedSearchText = model.searchText.components(separatedBy: .whitespaces)
59+
.filter { !$0.isEmpty }
60+
.joined(separator: "")
61+
62+
state = .loading
63+
updateSearchResults(with: normalizedSearchText)
64+
}
65+
66+
private func handleClearSearch() {
67+
model.searchText = ""
68+
state = .empty
69+
}
70+
71+
private func handleRemoveRecentSearch(_ search: String) {
72+
if let index = model.recentSearches.firstIndex(of: search) {
73+
model.recentSearches.remove(at: index)
74+
saveRecentSearches()
75+
}
76+
}
77+
78+
private func handleClearAllRecentSearches() {
79+
model.recentSearches.removeAll()
80+
saveRecentSearches()
81+
}
82+
83+
private func handleLocationSelection(_ result: SearchResult) {
84+
navigationManager.currentLocation = result.title
85+
navigationManager.pop(1)
86+
}
87+
88+
private func updateSearchResults(with query: String) {
89+
guard !query.isEmpty else {
90+
state = .empty
91+
return
92+
}
93+
94+
Task {
95+
do {
96+
let response = try await searchService.searchLocation(query: query)
97+
let results = response.locationResponseList.map { location in
98+
SearchResult(
99+
title: location.locationName,
100+
address: location.locationAddress ?? ""
101+
)
102+
}
103+
104+
await MainActor.run {
105+
state = .success(results: results)
106+
107+
if !model.recentSearches.contains(query) {
108+
model.recentSearches.insert(query, at: 0)
109+
if model.recentSearches.count > 6 {
110+
model.recentSearches.removeLast()
111+
}
112+
saveRecentSearches()
113+
}
114+
}
115+
} catch let error as SearchError {
116+
print("Search error: \(error.errorDescription)")
117+
state = .error(message: error.errorDescription)
118+
} catch {
119+
print("Unexpected error: \(error)")
120+
state = .error(message: "예상치 못한 오류가 발생했습니다")
121+
}
122+
}
123+
}
124+
func updateNavigationManager(_ manager: NavigationManager) {
125+
navigationManager = manager
126+
}
127+
128+
private func saveRecentSearches() {
129+
UserManager.shared.recentSearches = model.recentSearches
130+
}
131+
}

0 commit comments

Comments
 (0)