From cb0b136f36fff90f7ef161e0bfa88dcdcdcb5bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:06:58 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EB=81=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Features/Main/Home/HomeView.swift | 12 ++++++--- .../Features/Main/MainViewModel.swift | 16 ++++++++++- today-s-sound/Services/SpeechService.swift | 27 +++++++++++++++---- 3 files changed, 46 insertions(+), 9 deletions(-) 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..7faa740 100644 --- a/today-s-sound/Presentation/Features/Main/MainViewModel.swift +++ b/today-s-sound/Presentation/Features/Main/MainViewModel.swift @@ -1,4 +1,4 @@ -import Combine + import Combine import Foundation class MainViewModel: ObservableObject { @@ -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 From 07652fbbd6916efd6cc57fc43dd0322fded54f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:07:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=81=9D2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- today-s-sound/App/TodaySSoundApp.swift | 3 ++- today-s-sound/Presentation/Features/Main/MainViewModel.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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/MainViewModel.swift b/today-s-sound/Presentation/Features/Main/MainViewModel.swift index 7faa740..605c1fd 100644 --- a/today-s-sound/Presentation/Features/Main/MainViewModel.swift +++ b/today-s-sound/Presentation/Features/Main/MainViewModel.swift @@ -1,4 +1,4 @@ - import Combine +import Combine import Foundation class MainViewModel: ObservableObject {