diff --git a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.appiconset/Contents.json b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.appiconset/Contents.json deleted file mode 100644 index 2698331..0000000 --- a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.appiconset/Contents.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "images" : [ - { - "filename" : "Icon 오후 9.05.06.png", - "idiom" : "universal", - "platform" : "watchos", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git "a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.appiconset/Icon \354\230\244\355\233\204 9.05.06.png" "b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.appiconset/Icon \354\230\244\355\233\204 9.05.06.png" deleted file mode 100644 index a874d0e..0000000 Binary files "a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.appiconset/Icon \354\230\244\355\233\204 9.05.06.png" and /dev/null differ diff --git a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/Contents.json b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/Contents.json new file mode 100644 index 0000000..42a22c1 --- /dev/null +++ b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "MainCircle.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "MainCircle 1.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "MainCircle 2.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle 1.svg b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle 1.svg new file mode 100644 index 0000000..a296ccc --- /dev/null +++ b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle 1.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle 2.svg b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle 2.svg new file mode 100644 index 0000000..a296ccc --- /dev/null +++ b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle 2.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle.svg b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle.svg new file mode 100644 index 0000000..a296ccc --- /dev/null +++ b/hearo/HearoadWatch Watch App/Assets.xcassets/MainCircle.imageset/MainCircle.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hearo/HearoadWatch Watch App/ContentView.swift b/hearo/HearoadWatch Watch App/ContentView.swift index a5eb1cb..a4f73a7 100644 --- a/hearo/HearoadWatch Watch App/ContentView.swift +++ b/hearo/HearoadWatch Watch App/ContentView.swift @@ -12,24 +12,28 @@ struct ContentView: View { @ObservedObject var sessionManager = WatchSessionManager.shared var body: some View { + + + ZStack { // 배경색을 alert 상태에 따라 변경 sessionManager.isAlerting ? Color.red.edgesIgnoringSafeArea(.all) : Color.black.edgesIgnoringSafeArea(.all) - + + // 알림 상태에 따른 아이콘 표시 - if sessionManager.isAlerting { + if sessionManager.isAlerting { Image(sessionManager.alertImageName()) .resizable() .scaledToFit() .frame(width: 200, height: 200) .padding() - } else { - // 알림이 아닐 때는 MainCircle 이미지를 표시 - Image("MainCircle") - .resizable() - .frame(width: 200, height: 200) - .padding() - } + } + else{ + Image("MainCircle") + .resizable() + .frame(width: 200, height: 200) + .padding() + } } .onAppear { // 초기화 diff --git a/hearo/HearoadWatch Watch App/WatchSessionManager.swift b/hearo/HearoadWatch Watch App/WatchSessionManager.swift index cef3ba5..e085b83 100644 --- a/hearo/HearoadWatch Watch App/WatchSessionManager.swift +++ b/hearo/HearoadWatch Watch App/WatchSessionManager.swift @@ -43,18 +43,29 @@ class WatchSessionManager: NSObject, ObservableObject, WCSessionDelegate { case "Bicyclebell": return "Bicycle" // bicycle 이미지 이름 default: - return "exclamationmark.triangle.fill" // 기본 알림 아이콘 + return "Car" // 기본 알림 아이콘 } } // 3초 동안 알림 표시 후 기본 상태로 복구 func showAlert(with message: String) { + // "녹음 시작 전" 메시지는 무시 + guard message != "녹음 시작 전" else { + print("메시지 무시: \(message)") + return + } + alertMessage = message isAlerting = true // 강한 진동 알림 발생 playUrgentHapticPattern() + // UI 업데이트: 빨간 배경, 아이콘 표시 + DispatchQueue.main.async { + print("UI 업데이트 - 배경색: 빨간색, 메시지: \(message)") + } + // 3초 후에 기본 상태로 복구 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.resetAlert() @@ -79,6 +90,7 @@ class WatchSessionManager: NSObject, ObservableObject, WCSessionDelegate { func resetAlert() { alertMessage = "인식중" isAlerting = false + print("UI 상태 복구 - 배경색: 검정, 메시지: 기본 상태") } // WCSession 활성화 완료 시 호출되는 메서드 diff --git a/hearo/hearo.xcodeproj/project.pbxproj b/hearo/hearo.xcodeproj/project.pbxproj index 8055e5b..d1969d9 100644 --- a/hearo/hearo.xcodeproj/project.pbxproj +++ b/hearo/hearo.xcodeproj/project.pbxproj @@ -1015,8 +1015,8 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"hearo/Preview Content\""; - DEVELOPMENT_TEAM = GT56H2MYWV; + DEVELOPMENT_ASSET_PATHS = "hearo/Preview\\ Content HearoadWatch\\ Watch\\ App"; + DEVELOPMENT_TEAM = J5N8Y9F8Z8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = hearo/Info.plist; @@ -1049,8 +1049,8 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"hearo/Preview Content\""; - DEVELOPMENT_TEAM = GT56H2MYWV; + DEVELOPMENT_ASSET_PATHS = "hearo/Preview\\ Content HearoadWatch\\ Watch\\ App"; + DEVELOPMENT_TEAM = J5N8Y9F8Z8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = hearo/Info.plist; @@ -1155,8 +1155,8 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"HearoadWatch Watch App/Preview Content\""; - DEVELOPMENT_TEAM = GT56H2MYWV; + DEVELOPMENT_ASSET_PATHS = "hearo HearoadWatch\\ Watch\\ App/Preview\\ Content"; + DEVELOPMENT_TEAM = J5N8Y9F8Z8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "HearoadWatch-Watch-App-Info.plist"; @@ -1186,8 +1186,8 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"HearoadWatch Watch App/Preview Content\""; - DEVELOPMENT_TEAM = GT56H2MYWV; + DEVELOPMENT_ASSET_PATHS = "hearo HearoadWatch\\ Watch\\ App/Preview\\ Content"; + DEVELOPMENT_TEAM = J5N8Y9F8Z8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "HearoadWatch-Watch-App-Info.plist"; @@ -1295,7 +1295,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = GT56H2MYWV; + DEVELOPMENT_TEAM = J5N8Y9F8Z8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = LiveActivity/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; @@ -1323,7 +1323,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = GT56H2MYWV; + DEVELOPMENT_TEAM = J5N8Y9F8Z8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = LiveActivity/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = LiveActivity; diff --git a/hearo/hearo.xcodeproj/xcuserdata/pil_gaaang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/hearo/hearo.xcodeproj/xcuserdata/pil_gaaang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 635ce19..587da36 100644 --- a/hearo/hearo.xcodeproj/xcuserdata/pil_gaaang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/hearo/hearo.xcodeproj/xcuserdata/pil_gaaang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,41 +3,4 @@ uuid = "E612D0C1-8F2A-42B7-B849-5689FA63B147" type = "1" version = "2.0"> - - - - - -<<<<<<< Updated upstream -======= - - - - ->>>>>>> Stashed changes - diff --git a/hearo/hearo/Application/hearoApp.swift b/hearo/hearo/Application/hearoApp.swift index 7b8085a..097e6af 100644 --- a/hearo/hearo/Application/hearoApp.swift +++ b/hearo/hearo/Application/hearoApp.swift @@ -57,6 +57,8 @@ struct hearoApp: App { } final class AppRootManager: ObservableObject { + static let shared = AppRootManager() // 싱글톤 객체 + @Published var currentRoot: AppRoot = .splash { didSet { // working 상태로 전환될 때 오디오 수집 자동 시작 @@ -87,10 +89,7 @@ final class AppRootManager: ObservableObject { case warning } - init() { - self.hornSoundDetector = HornSoundDetector(appRootManager: self) - } - + // 스플래시 끝났을 때 호출 func determineNextRoot() { let hasSeenOnboarding = UserDefaults.standard.bool(forKey: "hasSeenOnboarding") diff --git a/hearo/hearo/Info.plist b/hearo/hearo/Info.plist index 8bdc516..c815fc7 100644 --- a/hearo/hearo/Info.plist +++ b/hearo/hearo/Info.plist @@ -2,16 +2,13 @@ + WKCompanionAppBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) UIAppFonts Pretendard-Light.otf Pretendard-Regular.otf Pretendard-Medium.otf - UIBackgroundModes - - audio - Audio - diff --git a/hearo/hearo/Resources/Assets.xcassets/SVGIcon/Icon.imageset/Contents.json b/hearo/hearo/Resources/Assets.xcassets/SVGIcon/Icon.imageset/Contents.json index b95e909..12bd45c 100644 --- a/hearo/hearo/Resources/Assets.xcassets/SVGIcon/Icon.imageset/Contents.json +++ b/hearo/hearo/Resources/Assets.xcassets/SVGIcon/Icon.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "filename" : "Icon 오후 9.05.06 1.png", + "filename" : "Icon 오후 9.05.06 1.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "Icon 오후 9.05.06.png", + "filename" : "Icon 오후 9.05.06.png", "idiom" : "universal", "scale" : "2x" }, diff --git a/hearo/hearo/Resources/Assets.xcassets/SVGIcon/StartButton.imageset/Contents.json b/hearo/hearo/Resources/Assets.xcassets/SVGIcon/StartButton.imageset/Contents.json index 4c2be32..f5e64f7 100644 --- a/hearo/hearo/Resources/Assets.xcassets/SVGIcon/StartButton.imageset/Contents.json +++ b/hearo/hearo/Resources/Assets.xcassets/SVGIcon/StartButton.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "시작하기.svg", + "filename" : "시작하기.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/hearo/hearo/Sources/Helper/HornSoundDetector.swift b/hearo/hearo/Sources/Helper/HornSoundDetector.swift index 27ce1c3..1e043dc 100644 --- a/hearo/hearo/Sources/Helper/HornSoundDetector.swift +++ b/hearo/hearo/Sources/Helper/HornSoundDetector.swift @@ -13,7 +13,7 @@ import UserNotifications import Combine import WatchConnectivity -class HornSoundDetector: NSObject, ObservableObject, WCSessionDelegate, SNResultsObserving { +class HornSoundDetector: NSObject, ObservableObject, SNResultsObserving { private var audioEngine: AVAudioEngine! private var inputNode: AVAudioInputNode! @@ -34,7 +34,6 @@ class HornSoundDetector: NSObject, ObservableObject, WCSessionDelegate, SNResult setupAudioSession() setupAudioEngine() setupSoundClassifier() - activateWCSession() } // MARK: - Audio Session Setup @@ -73,17 +72,6 @@ class HornSoundDetector: NSObject, ObservableObject, WCSessionDelegate, SNResult } } - // MARK: - WCSession Activation - private func activateWCSession() { - guard WCSession.isSupported() else { - print("WCSession이 지원되지 않음") - return - } - let session = WCSession.default - session.delegate = self - session.activate() - print("WCSession 활성화 요청 완료") - } // MARK: - Start Recording func startRecording() { @@ -123,14 +111,17 @@ class HornSoundDetector: NSObject, ObservableObject, WCSessionDelegate, SNResult } // MARK: - SNResultsObserving - @objc(request:didProduceResult:) func request(_ request: SNRequest, didProduce result: SNResult) { + @objc(request:didProduceResult:) + func request(_ request: SNRequest, didProduce result: SNResult) { guard let result = result as? SNClassificationResult else { return } DispatchQueue.main.async { if let topClassification = result.classifications.first { print("최상위 분류: \(topClassification.identifier), 신뢰도: \(topClassification.confidence)") - - // 관심 있는 소리만 처리 - if self.relevantSounds.contains(topClassification.identifier), topClassification.confidence >= 0.99 { + + // 관심 있는 소리와 신뢰도 기준 체크 + if self.relevantSounds.contains(topClassification.identifier), + topClassification.confidence >= 0.99 { + self.classificationResult = topClassification.identifier // 바로 업데이트 self.triggerWarningActions(for: topClassification.identifier) } else { print("관련 없는 소리 무시: \(topClassification.identifier)") @@ -138,54 +129,18 @@ class HornSoundDetector: NSObject, ObservableObject, WCSessionDelegate, SNResult } } } - // MARK: - Trigger Warning Actions private func triggerWarningActions(for sound: String) { - self.appRootManager.detectedSound = sound - self.appRootManager.currentRoot = .warning - - sendWarningToWatch(alert: sound) + appRootManager.detectedSound = sound + appRootManager.currentRoot = .warning print("⚠️ 경고 알림 처리 완료: \(sound)") + + // Watch로 바로 데이터 전달 + NotificationCenter.default.post(name: .detectedSoundNotification, object: nil, userInfo: ["sound": sound]) } + +} - private func sendWarningToWatch(alert: String) { - guard WCSession.isSupported() else { - print("WCSession이 지원되지 않음") - return - } - - guard WCSession.default.activationState == .activated else { - print("WCSession이 활성화되지 않음, 재활성화 시도") - WCSession.default.activate() - return - } - - guard WCSession.default.isReachable else { - print("애플워치가 연결되지 않음") - return - } - - let message = ["alert": alert] - WCSession.default.sendMessage(message, replyHandler: { response in - print("✅ 애플워치로 경고 메시지 전송 성공: \(alert)") - }) { error in - print("❌ 애플워치로 경고 메시지 전송 실패: \(error.localizedDescription)") - } - } - - // MARK: - WCSessionDelegate - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - print("WCSession 활성화 완료: \(activationState.rawValue)") - } - func sessionDidBecomeInactive(_ session: WCSession) { - // 세션이 비활성화될 때 호출됩니다. - print("WCSession 비활성화됨") - } - - func sessionDidDeactivate(_ session: WCSession) { - // 세션이 비활성화된 후 다시 활성화를 준비합니다. - print("WCSession 비활성화됨. 다시 활성화 준비") - WCSession.default.activate() - } - +extension Notification.Name { + static let detectedSoundNotification = Notification.Name("detectedSoundNotification") } diff --git a/hearo/hearo/Sources/Helper/SoundDetectorViewModel.swift b/hearo/hearo/Sources/Helper/SoundDetectorViewModel.swift index af9a276..8a02faf 100644 --- a/hearo/hearo/Sources/Helper/SoundDetectorViewModel.swift +++ b/hearo/hearo/Sources/Helper/SoundDetectorViewModel.swift @@ -8,41 +8,123 @@ import Foundation import Combine import WatchConnectivity -class SoundDetectorViewModel: NSObject, ObservableObject { +class SoundDetectorViewModel: NSObject, ObservableObject, WCSessionDelegate { @Published var isRecording = false @Published var classificationResult: String = "녹음 시작 전" @Published var confidence: Double = 0.0 - + private var hornSoundDetector: HornSoundDetector private var appRootManager: AppRootManager private var cancellables = Set() - + init(appRootManager: AppRootManager) { self.appRootManager = appRootManager self.hornSoundDetector = HornSoundDetector(appRootManager: appRootManager) super.init() setupBindings() - } + setupWCSession() // WCSession 설정 + // NotificationCenter를 통해 detectedSoundNotification 노티피케이션 수신 등록 + NotificationCenter.default.addObserver( + self, + selector: #selector(handleDetectedSoundNotification(_:)), + name: .detectedSoundNotification, + object: nil + ) + } + + @objc private func handleDetectedSoundNotification(_ notification: Notification) { + if let sound = notification.userInfo?["sound"] as? String { + print("SoundDetectorViewModel에서 전달받은 sound: \(sound)") + sendWarningToWatch(alert: sound) // 애플워치로 알림 전송 + } + } + deinit { + NotificationCenter.default.removeObserver(self, name: .detectedSoundNotification, object: nil) + } + private func setupBindings() { hornSoundDetector.$isRecording .assign(to: \.isRecording, on: self) .store(in: &cancellables) - + + // HornSoundDetector에서 Warning 발생 시 직접 Watch로 전달 hornSoundDetector.$classificationResult - .assign(to: \.classificationResult, on: self) + .sink { [weak self] result in + guard let self = self else { return } + print("SoundDetectorViewModel에서 전달받은 classificationResult: \(result)") + self.sendWarningToWatch(alert: result) // Watch에 전달 + } .store(in: &cancellables) - + hornSoundDetector.$confidence .assign(to: \.confidence, on: self) .store(in: &cancellables) } - + + private func setupWCSession() { + if WCSession.isSupported() { + let session = WCSession.default + session.delegate = self + session.activate() + } + } + func startRecording() { hornSoundDetector.startRecording() } - + func stopRecording() { hornSoundDetector.stopRecording() } + + private func sendWarningToWatch(alert: String) { + guard WCSession.default.activationState == .activated else { + print("WCSession이 활성화되지 않아 전송 보류: \(alert)") + return + } + + if WCSession.default.isReachable { + let message = ["alert": alert] + WCSession.default.sendMessage(message, replyHandler: nil) { error in + print("애플워치로 메시지 전송 실패: \(error.localizedDescription)") + } + } else { + do { + try WCSession.default.updateApplicationContext(["alert": alert]) + print("애플워치에 ApplicationContext로 데이터 전달 성공: \(alert)") + } catch { + print("애플워치 ApplicationContext 데이터 전달 실패: \(error.localizedDescription)") + } + } + } + + // WCSessionDelegate 메서드 + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + switch activationState { + case .notActivated: + print("WCSession이 활성화되지 않음.") + case .inactive: + print("WCSession이 비활성 상태.") + case .activated: + print("WCSession 활성화 성공.") + @unknown default: + print("알 수 없는 WCSession 상태.") + } + + if let error = error { + print("WCSession 활성화 실패: \(error.localizedDescription)") + } + } + func sessionDidBecomeInactive(_ session: WCSession) { + // 세션이 비활성화되었을 때 로그를 남기거나 필요한 작업을 수행 + print("WCSession이 비활성화되었습니다. 세션 상태: \(session.activationState.rawValue)") + } + + func sessionDidDeactivate(_ session: WCSession) { + // 세션이 비활성화된 후 다시 활성화를 준비 + print("WCSession이 비활성화되었습니다. 새로운 세션을 활성화합니다.") + WCSession.default.activate() + } + }