Skip to content

Commit 8a3ff0e

Browse files
committed
[feat]#2 매니저와 뷰모델, 서비스 파일 분리
- 두번 빠구먹고 성공했습니다 - VoiceSearchViewModel과 MainSearchViewModel이 같은 Manager를 구성하도록 수정했습니다
1 parent 981899f commit 8a3ff0e

File tree

6 files changed

+126
-136
lines changed

6 files changed

+126
-136
lines changed

BusRoad/Feature/MainSearch/MainSearchView.swift

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,36 @@ import SwiftUI
22

33
struct MainSearchView: View {
44
@EnvironmentObject private var coordinator: NavigationCoordinator
5-
@ObservedObject private var vm = MainSearchViewModel.shared
6-
5+
@StateObject private var vm = MainSearchViewModel()
6+
77
@State private var hasSubmitted = false
88
@State private var isSearchMode = false
9-
@State private var suppressKeyboardOnce = false
109
@FocusState private var isFocused: Bool
11-
10+
1211
var body: some View {
1312
Group {
1413
if isSearchMode {
1514
SearchModeSection(
16-
query: $vm.query,
15+
query: Binding(get: { vm.query }, set: { vm.query = $0 }),
1716
results: vm.results,
1817
isFocused: $isFocused,
1918
onBack: { exitSearchMode() },
2019
onSubmit: { performSearch() },
2120
onClear: { clearSearch() },
2221
onMicTap: {
2322
isFocused = false
24-
coordinator.push(.voiceSearch)}
23+
coordinator.push(.voiceSearch)
24+
}
2525
)
2626
} else {
2727
IntroSection(
28-
query: $vm.query,
28+
query: Binding(get: { vm.query }, set: { vm.query = $0 }),
2929
isFocused: $isFocused,
3030
onSubmit: { performSearch() },
3131
onMicTap: {
3232
isFocused = false
33-
coordinator.push(.voiceSearch) },
33+
coordinator.push(.voiceSearch)
34+
},
3435
onClear: { clearSearch() }
3536
)
3637
}
@@ -45,6 +46,7 @@ struct MainSearchView: View {
4546
DispatchQueue.main.async { isFocused = true }
4647
}
4748
}
49+
// ✅ SearchManager가 올린 전환 신호 감시
4850
.onChange(of: vm.shouldShowSearchMode) { _, show in
4951
if show {
5052
isSearchMode = true
@@ -53,32 +55,28 @@ struct MainSearchView: View {
5355
}
5456
}
5557
}
56-
}
5758

58-
// MARK: - Helpers
59-
private extension MainSearchView {
59+
// MARK: - Helpers
6060
@MainActor func exitSearchMode() {
6161
isSearchMode = false
6262
isFocused = false
6363
hasSubmitted = false
6464
vm.query = ""
65-
vm.results = []
65+
// vm.results는 매니저가 관리하니 굳이 초기화 필요 없음
6666
}
67-
67+
6868
@MainActor func performSearch() {
6969
isSearchMode = true
7070
hasSubmitted = true
71-
Task { await vm.search() } // vm이 내부에서 메인 업데이트하도록 설계 권장
71+
Task { await vm.search() }
7272
}
73-
73+
7474
@MainActor func clearSearch() {
7575
vm.query = ""
76-
vm.results = []
7776
hasSubmitted = false
7877
isFocused = true
7978
}
8079
}
81-
8280
//#Preview {
8381
// MainSearchView()
8482
// .environmentObject(NavigationCoordinator())

BusRoad/Feature/MainSearch/MainSearchViewModel.swift

Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,27 @@ import Foundation
44
@MainActor
55
final class MainSearchViewModel: ObservableObject {
66

7-
static let shared = MainSearchViewModel()
8-
9-
@Published var query: String = ""
10-
@Published var results: [NaverLocalItem] = []
11-
@Published var isLoading = false
12-
@Published var errorMessage: String?
13-
@Published var shouldShowSearchMode = false // 음성 검색 완료 후 검색 모드 표시용
14-
@Published var isFromVoiceSearch = false
15-
16-
private let manager: PlaceSearchManager
17-
18-
private init(manager: PlaceSearchManager = PlaceSearchManager()) {
19-
self.manager = manager
20-
}
21-
22-
/// 엔터/버튼에서 호출
23-
func search() async {
24-
errorMessage = nil
25-
let kw = query.trimmingCharacters(in: .whitespacesAndNewlines)
26-
guard !kw.isEmpty else { results = []; return }
27-
28-
isLoading = true
29-
defer { isLoading = false }
7+
let searchManager = SearchManager.shared
308

31-
do {
32-
results = try await manager.search(keyword: kw, display: 5, sort: "random")
33-
} catch {
34-
errorMessage = error.localizedDescription
35-
results = []
36-
}
9+
// SearchManager의 변경을 View로 릴레이 (UI 갱신 보장)
10+
private var bag = Set<AnyCancellable>()
11+
init() {
12+
searchManager.objectWillChange
13+
.sink { [weak self] _ in self?.objectWillChange.send() }
14+
.store(in: &bag)
3715
}
38-
39-
/// 음성 검색 완료 처리 (검색어 설정 + 검색 실행 + 모드 전환)
40-
func searchWithVoiceResult(_ text: String) async {
41-
query = text
42-
43-
isFromVoiceSearch = true
44-
shouldShowSearchMode = true
4516

46-
await search()
47-
}
48-
49-
/// 검색 모드 상태 초기화
50-
func resetSearchMode() {
51-
shouldShowSearchMode = false
52-
isFromVoiceSearch = false
17+
// 뷰에서 쓰기 편한 프록시
18+
var query: String {
19+
get { searchManager.query }
20+
set { searchManager.query = newValue }
5321
}
22+
var results: [NaverLocalItem] { searchManager.results }
23+
var shouldShowSearchMode: Bool { searchManager.shouldShowSearchMode }
24+
var isLoading: Bool { searchManager.isLoading }
25+
var errorMessage: String? { searchManager.errorMessage }
26+
27+
28+
func search() async { await searchManager.search() }
29+
func resetSearchMode() { searchManager.resetSearchMode() }
5430
}

BusRoad/Feature/VoiceSearch/VoiceSearchView.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@ struct VoiceSearchView: View {
44
@EnvironmentObject private var coordinator: NavigationCoordinator
55
@StateObject var vm = VoiceSearchViewModel()
66
@Environment(\.dismiss) private var dismiss
7-
7+
88
var onSearchCompleted: ((String) -> Void)? = nil
9-
9+
1010
var body: some View {
1111
ZStack {
1212
backgroundGradient
13-
13+
1414
VStack {
1515
Spacer()
16-
16+
1717
Text(vm.centerMessage)
1818
.font(.title2.weight(.medium))
1919
.foregroundColor(.white)
2020
.multilineTextAlignment(.center)
21-
21+
2222
Spacer()
23-
23+
2424
ZStack {
2525
if vm.showWaveAnimation {
2626
WaveRingsView()
@@ -36,14 +36,17 @@ struct VoiceSearchView: View {
3636
.foregroundColor(micIconColor)
3737
}
3838
}
39+
// 듣는 중/처리 중에는 살짝 눌린 느낌
3940
.scaleEffect(vm.isMicButtonEnabled ? 1.0 : 0.95)
4041
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: vm.state)
42+
.disabled(!vm.isMicButtonEnabled) // 준비/실패 외 상태에서는 탭 방지
4143
}
4244
.padding(.bottom, 60)
4345
.animation(.easeInOut(duration: 0.25), value: vm.showWaveAnimation)
4446
}
4547
.padding(.horizontal, 32)
46-
48+
49+
// 닫기 버튼
4750
VStack {
4851
HStack {
4952
Spacer()
@@ -62,13 +65,18 @@ struct VoiceSearchView: View {
6265
}
6366
.toolbar(.hidden, for: .navigationBar)
6467
.onAppear {
68+
// 1) 완료 시: 외부 콜백(있으면) -> pop
6569
vm.onSearchCompleted = { text in
6670
onSearchCompleted?(text)
6771
coordinator.pop()
6872
}
69-
vm.onDismiss = { coordinator.pop() }
73+
// 2) 닫기(X) 시 pop
74+
vm.onDismiss = { coordinator.pop() }
75+
// 3) 실제 리스닝 시작/바인딩
7076
vm.onAppear()
7177
}
72-
.onDisappear { vm.stopListening() }
78+
.onDisappear {
79+
vm.stopListening()
80+
}
7381
}
7482
}

0 commit comments

Comments
 (0)