diff --git a/today-s-sound/App/TodaySSoundApp.swift b/today-s-sound/App/TodaySSoundApp.swift index 8604ad4..a3ec86f 100644 --- a/today-s-sound/App/TodaySSoundApp.swift +++ b/today-s-sound/App/TodaySSoundApp.swift @@ -97,7 +97,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele // 등록된 사용자인지 확인 guard let userId = Keychain.getString(for: KeychainKey.userId), - let deviceSecret = Keychain.getString(for: KeychainKey.deviceSecret) else { + let deviceSecret = Keychain.getString(for: KeychainKey.deviceSecret) + else { // 미등록 사용자는 registerIfNeeded()에서 토큰과 함께 등록됨 print("ℹ️ [FCM] 미등록 사용자 - 서버 업데이트 생략 (추후 등록 시 전송)") print("====================================\n") diff --git a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift index 5576577..a88df3c 100644 --- a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift +++ b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift @@ -34,11 +34,17 @@ struct HomeView: View { Button( action: { if speechService.isSpeaking { - speechService.stop() + // 재생 중 → 일시정지 + viewModel.pausePlayback() + } else if viewModel.isPaused { + // 일시정지 상태 → 이어서 재생 + viewModel.resumePlayback() } else { - // 홈 피드가 있으면 첫 번째 피드 아이템 재생 + // 정지 상태 → 처음부터 재생 if !viewModel.homeFeedItems.isEmpty { viewModel.playFirstFeedItem() + } else { + SpeechService.shared.speak(text: "아직 구독한 페이지가 없거나 새로운 글이 없습니다.") } } }, @@ -50,7 +56,7 @@ struct HomeView: View { .padding(20) } ) - .accessibilityLabel(speechService.isSpeaking ? "재생 중단" : "재생 시작") + .accessibilityLabel(speechService.isSpeaking ? "일시정지" : viewModel.isPaused ? "이어서 재생" : "재생 시작") .padding(.bottom, 60) Spacer() diff --git a/today-s-sound/Presentation/Features/Main/MainViewModel.swift b/today-s-sound/Presentation/Features/Main/MainViewModel.swift index 3febf68..605c1fd 100644 --- a/today-s-sound/Presentation/Features/Main/MainViewModel.swift +++ b/today-s-sound/Presentation/Features/Main/MainViewModel.swift @@ -7,6 +7,7 @@ class MainViewModel: ObservableObject { @Published var recentAlerts: [Alert] = [] @Published var homeFeedItems: [HomeFeedItemResponse] = [] @Published var isLoading: Bool = false + @Published var isPaused: Bool = false @Published var errorMessage: String? private let apiService: APIService @@ -163,12 +164,25 @@ class MainViewModel: ObservableObject { playCurrentGroup() } + /// 일시정지 + func pausePlayback() { + SpeechService.shared.pause() + isPaused = true + } + + /// 이어서 재생 + func resumePlayback() { + SpeechService.shared.resume() + isPaused = false + } + /// 재생 중단 시 큐 초기화 func stopPlayback() { SpeechService.shared.stop() playbackQueue = [] currentGroupIndex = 0 currentItemIndex = 0 + isPaused = false } /// 현재 그룹 재생 (카테고리명 먼저, 그 다음 summary들) diff --git a/today-s-sound/Services/SpeechService.swift b/today-s-sound/Services/SpeechService.swift index feaf6f4..84fe2a4 100644 --- a/today-s-sound/Services/SpeechService.swift +++ b/today-s-sound/Services/SpeechService.swift @@ -10,6 +10,7 @@ class SpeechService: NSObject, ObservableObject, AVSpeechSynthesizerDelegate { let didFinishSpeaking = PassthroughSubject() @Published var isSpeaking: Bool = false + @Published var isPaused: Bool = false override private init() { super.init() @@ -39,21 +40,37 @@ class SpeechService: NSObject, ObservableObject, AVSpeechSynthesizerDelegate { } // Stop any speaking in progress before starting a new one - if synthesizer.isSpeaking { + if synthesizer.isSpeaking || isPaused { synthesizer.stopSpeaking(at: .immediate) - // 중단 이벤트는 didFinishSpeaking으로 전달하지 않음 } + isPaused = false isSpeaking = true synthesizer.speak(utterance) } - func stop() { + func pause() { if synthesizer.isSpeaking { - synthesizer.stopSpeaking(at: .immediate) + isSpeaking = false + isPaused = true + synthesizer.pauseSpeaking(at: .immediate) + } + } + + func resume() { + if isPaused { + isPaused = false + isSpeaking = true + synthesizer.continueSpeaking() } + } + + func stop() { isSpeaking = false - // stop() 호출 시에는 didFinishSpeaking 이벤트를 보내지 않음 (의도적 중단) + isPaused = false + if synthesizer.isSpeaking || synthesizer.isPaused { + synthesizer.stopSpeaking(at: .immediate) + } } // MARK: - AVSpeechSynthesizerDelegate