From da4b6fd0b87cc4cdbdf2df25da2da6772d3359f8 Mon Sep 17 00:00:00 2001 From: YunDaeHyeon Date: Wed, 4 Feb 2026 20:10:19 +0900 Subject: [PATCH 01/17] =?UTF-8?q?refactor:=20Browser=EC=9D=98=20AsyncStrea?= =?UTF-8?q?m=EC=9D=84=20BrowserStreamManager=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Browser는 계산 프로퍼티로 스트림을 노출 - HeartBeeater는 스트림 매니저의 yield 메서드 사용 - 테스트 코드 모두 통과로 동등성 확인 완료 --- .../Camera/Browser/BrowserStreamManager.swift | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift new file mode 100644 index 00000000..ae97ccd6 --- /dev/null +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift @@ -0,0 +1,86 @@ +// +// BrowserStreamManager.swift +// mirroringBooth +// +// Created by 윤대현 on 2026-02-04. +// + +import Foundation + +/// Browser의 모든 AsyncStream과 Continuation을 관리하는 매니저 +/// 이벤트 발행(yield)과 스트림 접근을 담당합니다. +final class BrowserStreamManager { + + // MARK: - Browsing + + /// 기기 검색 및 연결 전용 이벤트 스트림 + let browsingEventStream: AsyncStream + private let browsingEventContinuation: AsyncStream.Continuation + + // MARK: - Camera Stream + + /// 카메라 촬영 및 전송 전용 이벤트 스트림 + let cameraStreamEventStream: AsyncStream + private let cameraStreamEventContinuation: AsyncStream.Continuation + + // MARK: - Heartbeat Streams + + /// BrowsingStore 전용 Heartbeat 스트림 + let browsingHeartbeatStream: AsyncStream + let browsingHeartbeatContinuation: AsyncStream.Continuation + + /// ConnectionCheckStore 전용 Heartbeat 스트림 + let connectionCheckHeartbeatStream: AsyncStream + let connectionCheckHeartbeatContinuation: AsyncStream.Continuation + + /// CameraPreviewStore 전용 Heartbeat 스트림 + let cameraPreviewHeartbeatStream: AsyncStream + let cameraPreviewHeartbeatContinuation: AsyncStream.Continuation + + + init() { + (self.browsingEventStream, self.browsingEventContinuation) = AsyncStream.makeStream( + of: BrowsingEvents.self + ) + + (self.cameraStreamEventStream, self.cameraStreamEventContinuation) = AsyncStream.makeStream( + of: CameraStreamEvents.self, + bufferingPolicy: .bufferingNewest(1) + ) + + (self.browsingHeartbeatStream, self.browsingHeartbeatContinuation) = AsyncStream.makeStream( + of: HeartBeatEvents.self, + bufferingPolicy: .bufferingNewest(1) + ) + + (self.connectionCheckHeartbeatStream, self.connectionCheckHeartbeatContinuation) = AsyncStream.makeStream( + of: HeartBeatEvents.self, + bufferingPolicy: .bufferingNewest(1) + ) + + (self.cameraPreviewHeartbeatStream, self.cameraPreviewHeartbeatContinuation) = AsyncStream.makeStream( + of: HeartBeatEvents.self, + bufferingPolicy: .bufferingNewest(1) + ) + } + + func yieldBrowsingEvent(_ event: BrowsingEvents) { + browsingEventContinuation.yield(event) + } + + func yieldCameraStreamEvent(_ event: CameraStreamEvents) { + cameraStreamEventContinuation.yield(event) + } + + func yieldHeartbeatTimeoutToAll() { + browsingHeartbeatContinuation.yield(.heartbeatTimeout) + connectionCheckHeartbeatContinuation.yield(.heartbeatTimeout) + cameraPreviewHeartbeatContinuation.yield(.heartbeatTimeout) + } + + func yieldRemoteHeartbeatTimeoutToAll() { + browsingHeartbeatContinuation.yield(.remoteHeartbeatTimeout) + connectionCheckHeartbeatContinuation.yield(.remoteHeartbeatTimeout) + cameraPreviewHeartbeatContinuation.yield(.remoteHeartbeatTimeout) + } +} From 0a4e01e1526389fc0a2bb8510bbb3ab39a1b151d Mon Sep 17 00:00:00 2001 From: YunDaeHyeon Date: Wed, 4 Feb 2026 21:13:35 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20BrowsingManager=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MCNearbyServiceBrowserDelegate 분리 - discoveredPeers 상태 관리 분리 - Browser는 browsingManager 인스턴스를 사용하도록 개선 - delegate extenstion 제거 --- .../Camera/Browser/BrowserStreamManager.swift | 4 +- .../Camera/Browser/BrowsingManager.swift | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingManager.swift diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift index ae97ccd6..1d53901c 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift @@ -7,8 +7,7 @@ import Foundation -/// Browser의 모든 AsyncStream과 Continuation을 관리하는 매니저 -/// 이벤트 발행(yield)과 스트림 접근을 담당합니다. +/// Browser의 Stream과 Continuation을 관리하는 매니저 final class BrowserStreamManager { // MARK: - Browsing @@ -37,7 +36,6 @@ final class BrowserStreamManager { let cameraPreviewHeartbeatStream: AsyncStream let cameraPreviewHeartbeatContinuation: AsyncStream.Continuation - init() { (self.browsingEventStream, self.browsingEventContinuation) = AsyncStream.makeStream( of: BrowsingEvents.self diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingManager.swift new file mode 100644 index 00000000..0a119fb6 --- /dev/null +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingManager.swift @@ -0,0 +1,89 @@ +// +// BrowsingManager.swift +// mirroringBooth +// +// Created by 윤대현 on 2026-02-04. +// + +import MultipeerConnectivity +import OSLog + +/// 기기 탐색을 담당하는 매니저 +final class BrowsingManager: NSObject { + + private let logger = Logger.browsingManager + private let browser: MCNearbyServiceBrowser + private let peerID: MCPeerID + private let streamManager: BrowserStreamManager + + /// 발견된 Peer 목록 + private(set) var discoveredPeers: [String: (peer: MCPeerID, type: DeviceType)] = [:] + + init(peerID: MCPeerID, serviceType: String, streamManager: BrowserStreamManager) { + self.peerID = peerID + self.streamManager = streamManager + self.browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType) + super.init() + browser.delegate = self + } + + func startSearching() { + browser.stopBrowsingForPeers() + browser.startBrowsingForPeers() + logger.info("주변 기기를 검색합니다.") + } + + func stopSearching() { + browser.stopBrowsingForPeers() + logger.info("주변 기기 검색을 중지합니다.") + } + + /// 특정 기기에게 연결 초대를 전송합니다. + func invitePeer(_ deviceID: String, to session: MCSession, withContext context: Data?, timeout: TimeInterval) { + guard let (peer, _) = discoveredPeers[deviceID] else { + logger.warning("[연결 실패] 기기를 찾을 수 없음 : \(deviceID)") + return + } + browser.invitePeer(peer, to: session, withContext: context, timeout: timeout) + logger.info("[\(deviceID)]에게 연결 요청을 전송했습니다.") + } + + /// 특정 기기가 발견되었는지 확인합니다. + func getPeer(for deviceID: String) -> (peer: MCPeerID, type: DeviceType)? { + return discoveredPeers[deviceID] + } +} + +// MARK: - MCNearbyServiceBrowserDelegate + +extension BrowsingManager: MCNearbyServiceBrowserDelegate { + func browser(_ browser: MCNearbyServiceBrowser, + foundPeer peerID: MCPeerID, + withDiscoveryInfo info: [String: String]?) { + logger.info("발견된 기기: \(peerID.displayName)") + guard let deviceTypeString = info?["deviceType"], + let deviceType = DeviceType.from(string: deviceTypeString) + else { return } + + self.discoveredPeers[peerID.displayName] = (peer: peerID, type: deviceType) + let device = NearbyDevice( + id: peerID.displayName, + state: .notConnected, + type: deviceType + ) + streamManager.yieldBrowsingEvent(.deviceFound(device)) + } + + func browser(_ browser: MCNearbyServiceBrowser, + lostPeer peerID: MCPeerID) { + logger.info("사라진 기기: \(peerID.displayName)") + let deviceType = self.discoveredPeers[peerID.displayName]?.type ?? .unknown + self.discoveredPeers.removeValue(forKey: peerID.displayName) + let device = NearbyDevice( + id: peerID.displayName, + state: .notConnected, + type: deviceType + ) + streamManager.yieldBrowsingEvent(.deviceLost(device)) + } +} From 511fab04a7a462ac6225d8c13023809e196bc2a9 Mon Sep 17 00:00:00 2001 From: YunDaeHyeon Date: Wed, 4 Feb 2026 21:32:28 +0900 Subject: [PATCH 03/17] =?UTF-8?q?refactor:=20BrowserCommand=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - executeCommand 메서드를 CommandManager의 execute(data:)로 분리 - BrowserCommandDelegate Protocol 정의 - Browser에서 delegate 채택 --- .../Browser/BrowserCommandManager.swift | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserCommandManager.swift diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserCommandManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserCommandManager.swift new file mode 100644 index 00000000..7362e467 --- /dev/null +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserCommandManager.swift @@ -0,0 +1,62 @@ +// +// BrowserCommandManager.swift +// mirroringBooth +// +// Created by 윤대현 on 2026-02-04. +// + +import Foundation + +protocol BrowserCommandDelegate: AnyObject { + func capturePhoto() + func sendRemoteCommand(_ command: Browser.RemoteDeviceCommand) + var onRemoteModeCommand: (() -> Void)? { get } + var onSelectedTimerModeCommand: (() -> Void)? { get } + var mirroringHeartBeater: HeartBeater { get } + var remoteHeartBeater: HeartBeater? { get } +} + +/// 명령 수신 및 실행을 담당하는 매니저 +final class BrowserCommandManager { + + private let streamManager: BrowserStreamManager + weak var delegate: BrowserCommandDelegate? + + init(streamManager: BrowserStreamManager) { + self.streamManager = streamManager + } + + func execute(data: Data) { + guard let command = String(data: data, encoding: .utf8) else { return } + guard let type = Advertiser.CameraDeviceCommand(rawValue: command) else { return } + + switch type { + case .capturePhoto: + DispatchQueue.main.async { [weak self] in + self?.delegate?.capturePhoto() + } + case .startTransfer: + DispatchQueue.main.async { [weak self] in + self?.streamManager.yieldCameraStreamEvent(.startTransfer) + self?.delegate?.sendRemoteCommand(.navigateToRemoteComplete) + } + case .setRemoteMode: + DispatchQueue.main.async { [weak self] in + self?.delegate?.onRemoteModeCommand?() + self?.delegate?.sendRemoteCommand(.navigateToRemoteCapture) + } + case .selectedTimerMode: + DispatchQueue.main.async { [weak self] in + self?.delegate?.onSelectedTimerModeCommand?() + self?.delegate?.sendRemoteCommand(.navigateToHome) + } + case .heartBeat: + delegate?.mirroringHeartBeater.beat() + case .remoteHeartBeat: + delegate?.remoteHeartBeater?.beat() + case .stopHeartBeat: + delegate?.mirroringHeartBeater.stop() + delegate?.remoteHeartBeater?.stop() + } + } +} From f0cd586ca79f70400530f72ef2da07948cec9524 Mon Sep 17 00:00:00 2001 From: YunDaeHyeon Date: Wed, 4 Feb 2026 21:44:12 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20HeartBeat=20Continuation=20privat?= =?UTF-8?q?e=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Device/Camera/Browser/BrowserStreamManager.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift index 1d53901c..ba4667a7 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift @@ -26,15 +26,15 @@ final class BrowserStreamManager { /// BrowsingStore 전용 Heartbeat 스트림 let browsingHeartbeatStream: AsyncStream - let browsingHeartbeatContinuation: AsyncStream.Continuation + private let browsingHeartbeatContinuation: AsyncStream.Continuation /// ConnectionCheckStore 전용 Heartbeat 스트림 let connectionCheckHeartbeatStream: AsyncStream - let connectionCheckHeartbeatContinuation: AsyncStream.Continuation + private let connectionCheckHeartbeatContinuation: AsyncStream.Continuation /// CameraPreviewStore 전용 Heartbeat 스트림 let cameraPreviewHeartbeatStream: AsyncStream - let cameraPreviewHeartbeatContinuation: AsyncStream.Continuation + private let cameraPreviewHeartbeatContinuation: AsyncStream.Continuation init() { (self.browsingEventStream, self.browsingEventContinuation) = AsyncStream.makeStream( From 2343a8cc0c38127373a9bbef463c6b97bd1a1cbd Mon Sep 17 00:00:00 2001 From: YunDaeHyeon Date: Wed, 4 Feb 2026 21:52:13 +0900 Subject: [PATCH 05/17] =?UTF-8?q?refactor:=20Browser=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20enum=20Command=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 Browser.MirroringDeviceCommand, Browser.RemoteDeviceCommand 사용처에서 Browser. 만 제거 - BrowserManager 폴더 생성 및 관련 파일 이동 --- .../BrowserCommandManager.swift | 2 +- .../BrowserManager/BrowserCommands.swift | 25 +++++++++++++++++++ .../BrowserStreamManager.swift | 0 .../BrowsingManager.swift | 0 4 files changed, 26 insertions(+), 1 deletion(-) rename mirroringBooth/mirroringBooth/Device/Camera/Browser/{ => BrowserManager}/BrowserCommandManager.swift (96%) create mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift rename mirroringBooth/mirroringBooth/Device/Camera/Browser/{ => BrowserManager}/BrowserStreamManager.swift (100%) rename mirroringBooth/mirroringBooth/Device/Camera/Browser/{ => BrowserManager}/BrowsingManager.swift (100%) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserCommandManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommandManager.swift similarity index 96% rename from mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserCommandManager.swift rename to mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommandManager.swift index 7362e467..1d011ea6 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserCommandManager.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommandManager.swift @@ -9,7 +9,7 @@ import Foundation protocol BrowserCommandDelegate: AnyObject { func capturePhoto() - func sendRemoteCommand(_ command: Browser.RemoteDeviceCommand) + func sendRemoteCommand(_ command: RemoteDeviceCommand) var onRemoteModeCommand: (() -> Void)? { get } var onSelectedTimerModeCommand: (() -> Void)? { get } var mirroringHeartBeater: HeartBeater { get } diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift new file mode 100644 index 00000000..87f5eb7b --- /dev/null +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift @@ -0,0 +1,25 @@ +// +// BrowserCommands.swift +// mirroringBooth +// +// Created by 윤대현 on 2/4/26. +// + +enum MirroringDeviceCommand: String { + case navigateToSelectModeWithRemote + case navigateToSelectModeWithoutRemote + case switchSelectModeView + case onStoreAllPhotos // 사진 10장 저장 시작 명령(from camera) + case onUpdateCaptureCount // 리모트 기기에서 카메라 캡처 요청 보내기 + case heartBeat + case captureEffect +} + +enum RemoteDeviceCommand: String { + case navigateToRemoteCapture + case navigateToRemoteComplete + case navigateToRemoteConnected + case navigateToHome + case noticeIsRemoteDevice + case heartBeat +} diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserStreamManager.swift similarity index 100% rename from mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserStreamManager.swift rename to mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserStreamManager.swift diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowsingManager.swift similarity index 100% rename from mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingManager.swift rename to mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowsingManager.swift From a93ce98c539377bddd93d343edcb566734574cc5 Mon Sep 17 00:00:00 2001 From: YunDaeHyeon Date: Thu, 5 Feb 2026 02:28:16 +0900 Subject: [PATCH 06/17] =?UTF-8?q?chore:=20=ED=8F=B4=EB=8D=94=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20BrowserManager=20->=20Manager=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BrowserCommandManager.swift | 62 ------------- .../BrowserManager/BrowserCommands.swift | 25 ------ .../BrowserManager/BrowserStreamManager.swift | 84 ----------------- .../BrowserManager/BrowsingManager.swift | 89 ------------------- 4 files changed, 260 deletions(-) delete mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommandManager.swift delete mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift delete mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserStreamManager.swift delete mode 100644 mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowsingManager.swift diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommandManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommandManager.swift deleted file mode 100644 index 1d011ea6..00000000 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommandManager.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// BrowserCommandManager.swift -// mirroringBooth -// -// Created by 윤대현 on 2026-02-04. -// - -import Foundation - -protocol BrowserCommandDelegate: AnyObject { - func capturePhoto() - func sendRemoteCommand(_ command: RemoteDeviceCommand) - var onRemoteModeCommand: (() -> Void)? { get } - var onSelectedTimerModeCommand: (() -> Void)? { get } - var mirroringHeartBeater: HeartBeater { get } - var remoteHeartBeater: HeartBeater? { get } -} - -/// 명령 수신 및 실행을 담당하는 매니저 -final class BrowserCommandManager { - - private let streamManager: BrowserStreamManager - weak var delegate: BrowserCommandDelegate? - - init(streamManager: BrowserStreamManager) { - self.streamManager = streamManager - } - - func execute(data: Data) { - guard let command = String(data: data, encoding: .utf8) else { return } - guard let type = Advertiser.CameraDeviceCommand(rawValue: command) else { return } - - switch type { - case .capturePhoto: - DispatchQueue.main.async { [weak self] in - self?.delegate?.capturePhoto() - } - case .startTransfer: - DispatchQueue.main.async { [weak self] in - self?.streamManager.yieldCameraStreamEvent(.startTransfer) - self?.delegate?.sendRemoteCommand(.navigateToRemoteComplete) - } - case .setRemoteMode: - DispatchQueue.main.async { [weak self] in - self?.delegate?.onRemoteModeCommand?() - self?.delegate?.sendRemoteCommand(.navigateToRemoteCapture) - } - case .selectedTimerMode: - DispatchQueue.main.async { [weak self] in - self?.delegate?.onSelectedTimerModeCommand?() - self?.delegate?.sendRemoteCommand(.navigateToHome) - } - case .heartBeat: - delegate?.mirroringHeartBeater.beat() - case .remoteHeartBeat: - delegate?.remoteHeartBeater?.beat() - case .stopHeartBeat: - delegate?.mirroringHeartBeater.stop() - delegate?.remoteHeartBeater?.stop() - } - } -} diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift deleted file mode 100644 index 87f5eb7b..00000000 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserCommands.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// BrowserCommands.swift -// mirroringBooth -// -// Created by 윤대현 on 2/4/26. -// - -enum MirroringDeviceCommand: String { - case navigateToSelectModeWithRemote - case navigateToSelectModeWithoutRemote - case switchSelectModeView - case onStoreAllPhotos // 사진 10장 저장 시작 명령(from camera) - case onUpdateCaptureCount // 리모트 기기에서 카메라 캡처 요청 보내기 - case heartBeat - case captureEffect -} - -enum RemoteDeviceCommand: String { - case navigateToRemoteCapture - case navigateToRemoteComplete - case navigateToRemoteConnected - case navigateToHome - case noticeIsRemoteDevice - case heartBeat -} diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserStreamManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserStreamManager.swift deleted file mode 100644 index ba4667a7..00000000 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowserStreamManager.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// BrowserStreamManager.swift -// mirroringBooth -// -// Created by 윤대현 on 2026-02-04. -// - -import Foundation - -/// Browser의 Stream과 Continuation을 관리하는 매니저 -final class BrowserStreamManager { - - // MARK: - Browsing - - /// 기기 검색 및 연결 전용 이벤트 스트림 - let browsingEventStream: AsyncStream - private let browsingEventContinuation: AsyncStream.Continuation - - // MARK: - Camera Stream - - /// 카메라 촬영 및 전송 전용 이벤트 스트림 - let cameraStreamEventStream: AsyncStream - private let cameraStreamEventContinuation: AsyncStream.Continuation - - // MARK: - Heartbeat Streams - - /// BrowsingStore 전용 Heartbeat 스트림 - let browsingHeartbeatStream: AsyncStream - private let browsingHeartbeatContinuation: AsyncStream.Continuation - - /// ConnectionCheckStore 전용 Heartbeat 스트림 - let connectionCheckHeartbeatStream: AsyncStream - private let connectionCheckHeartbeatContinuation: AsyncStream.Continuation - - /// CameraPreviewStore 전용 Heartbeat 스트림 - let cameraPreviewHeartbeatStream: AsyncStream - private let cameraPreviewHeartbeatContinuation: AsyncStream.Continuation - - init() { - (self.browsingEventStream, self.browsingEventContinuation) = AsyncStream.makeStream( - of: BrowsingEvents.self - ) - - (self.cameraStreamEventStream, self.cameraStreamEventContinuation) = AsyncStream.makeStream( - of: CameraStreamEvents.self, - bufferingPolicy: .bufferingNewest(1) - ) - - (self.browsingHeartbeatStream, self.browsingHeartbeatContinuation) = AsyncStream.makeStream( - of: HeartBeatEvents.self, - bufferingPolicy: .bufferingNewest(1) - ) - - (self.connectionCheckHeartbeatStream, self.connectionCheckHeartbeatContinuation) = AsyncStream.makeStream( - of: HeartBeatEvents.self, - bufferingPolicy: .bufferingNewest(1) - ) - - (self.cameraPreviewHeartbeatStream, self.cameraPreviewHeartbeatContinuation) = AsyncStream.makeStream( - of: HeartBeatEvents.self, - bufferingPolicy: .bufferingNewest(1) - ) - } - - func yieldBrowsingEvent(_ event: BrowsingEvents) { - browsingEventContinuation.yield(event) - } - - func yieldCameraStreamEvent(_ event: CameraStreamEvents) { - cameraStreamEventContinuation.yield(event) - } - - func yieldHeartbeatTimeoutToAll() { - browsingHeartbeatContinuation.yield(.heartbeatTimeout) - connectionCheckHeartbeatContinuation.yield(.heartbeatTimeout) - cameraPreviewHeartbeatContinuation.yield(.heartbeatTimeout) - } - - func yieldRemoteHeartbeatTimeoutToAll() { - browsingHeartbeatContinuation.yield(.remoteHeartbeatTimeout) - connectionCheckHeartbeatContinuation.yield(.remoteHeartbeatTimeout) - cameraPreviewHeartbeatContinuation.yield(.remoteHeartbeatTimeout) - } -} diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowsingManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowsingManager.swift deleted file mode 100644 index 0a119fb6..00000000 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowserManager/BrowsingManager.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// BrowsingManager.swift -// mirroringBooth -// -// Created by 윤대현 on 2026-02-04. -// - -import MultipeerConnectivity -import OSLog - -/// 기기 탐색을 담당하는 매니저 -final class BrowsingManager: NSObject { - - private let logger = Logger.browsingManager - private let browser: MCNearbyServiceBrowser - private let peerID: MCPeerID - private let streamManager: BrowserStreamManager - - /// 발견된 Peer 목록 - private(set) var discoveredPeers: [String: (peer: MCPeerID, type: DeviceType)] = [:] - - init(peerID: MCPeerID, serviceType: String, streamManager: BrowserStreamManager) { - self.peerID = peerID - self.streamManager = streamManager - self.browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType) - super.init() - browser.delegate = self - } - - func startSearching() { - browser.stopBrowsingForPeers() - browser.startBrowsingForPeers() - logger.info("주변 기기를 검색합니다.") - } - - func stopSearching() { - browser.stopBrowsingForPeers() - logger.info("주변 기기 검색을 중지합니다.") - } - - /// 특정 기기에게 연결 초대를 전송합니다. - func invitePeer(_ deviceID: String, to session: MCSession, withContext context: Data?, timeout: TimeInterval) { - guard let (peer, _) = discoveredPeers[deviceID] else { - logger.warning("[연결 실패] 기기를 찾을 수 없음 : \(deviceID)") - return - } - browser.invitePeer(peer, to: session, withContext: context, timeout: timeout) - logger.info("[\(deviceID)]에게 연결 요청을 전송했습니다.") - } - - /// 특정 기기가 발견되었는지 확인합니다. - func getPeer(for deviceID: String) -> (peer: MCPeerID, type: DeviceType)? { - return discoveredPeers[deviceID] - } -} - -// MARK: - MCNearbyServiceBrowserDelegate - -extension BrowsingManager: MCNearbyServiceBrowserDelegate { - func browser(_ browser: MCNearbyServiceBrowser, - foundPeer peerID: MCPeerID, - withDiscoveryInfo info: [String: String]?) { - logger.info("발견된 기기: \(peerID.displayName)") - guard let deviceTypeString = info?["deviceType"], - let deviceType = DeviceType.from(string: deviceTypeString) - else { return } - - self.discoveredPeers[peerID.displayName] = (peer: peerID, type: deviceType) - let device = NearbyDevice( - id: peerID.displayName, - state: .notConnected, - type: deviceType - ) - streamManager.yieldBrowsingEvent(.deviceFound(device)) - } - - func browser(_ browser: MCNearbyServiceBrowser, - lostPeer peerID: MCPeerID) { - logger.info("사라진 기기: \(peerID.displayName)") - let deviceType = self.discoveredPeers[peerID.displayName]?.type ?? .unknown - self.discoveredPeers.removeValue(forKey: peerID.displayName) - let device = NearbyDevice( - id: peerID.displayName, - state: .notConnected, - type: deviceType - ) - streamManager.yieldBrowsingEvent(.deviceLost(device)) - } -} From cc8b45777184a4ec2296237bcde2e70f081e1ad0 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 11:49:03 +0900 Subject: [PATCH 07/17] =?UTF-8?q?fix:=20CameraPreview=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=AA=A8=ED=8A=B8=20=EA=B8=B0=EA=B8=B0=20=EB=81=8A?= =?UTF-8?q?=EA=B9=80=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mirroringBooth/App/RootView.swift | 4 +- .../mirroringBooth/Core/Router.swift | 3 +- .../Device/Camera/Browser/BrowsingView.swift | 6 ++- .../ConnectionCheckStore.swift | 7 ++- .../ConnectionCheck/ConnectionCheckView.swift | 6 ++- .../Camera/Streaming/CameraPreview.swift | 12 ++++- .../Camera/Streaming/CameraPreviewStore.swift | 46 +++++++++++++++---- 7 files changed, 65 insertions(+), 19 deletions(-) diff --git a/mirroringBooth/mirroringBooth/App/RootView.swift b/mirroringBooth/mirroringBooth/App/RootView.swift index 169ab27d..3c21c9dd 100644 --- a/mirroringBooth/mirroringBooth/App/RootView.swift +++ b/mirroringBooth/mirroringBooth/App/RootView.swift @@ -23,8 +23,8 @@ struct RootView: View { case .advertising: AdvertisingView() - case .connectionList(let list, let browser): - ConnectionCheckView(list, browser: browser) + case .connectionList(let list, let browser, let watchConnectionManager): + ConnectionCheckView(list, browser: browser, watchConnectionManager: watchConnectionManager) case .completion: StreamingCompletionView() diff --git a/mirroringBooth/mirroringBooth/Core/Router.swift b/mirroringBooth/mirroringBooth/Core/Router.swift index 1809398b..8cdb786b 100644 --- a/mirroringBooth/mirroringBooth/Core/Router.swift +++ b/mirroringBooth/mirroringBooth/Core/Router.swift @@ -27,7 +27,7 @@ final class Router { enum CameraRoute: Hashable { case browsing case advertising - case connectionList(ConnectionList, Browser) + case connectionList(ConnectionList, Browser, WatchConnectionManager) case completion } @@ -35,6 +35,7 @@ struct ConnectionList: Hashable { let cameraName: String let mirroringName: String let remoteName: String? + let remoteType: DeviceType? } enum MirroringRoute: Hashable { diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift index 9462d54d..9e2a9ff2 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift @@ -93,9 +93,11 @@ struct BrowsingView: View { ConnectionList( cameraName: store.browser.myDeviceName, mirroringName: mirroringDevice.id, - remoteName: store.state.remoteDevice?.id ?? nil + remoteName: store.state.remoteDevice?.id, + remoteType: store.state.remoteDevice?.type ), - store.browser + store.browser, + store.watchConnectionManager ) ) } diff --git a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift index b886c78b..0554773b 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift @@ -36,18 +36,23 @@ final class ConnectionCheckStore: StoreProtocol { private(set) var state: State = .init() let browser: Browser + let watchConnectionManager: WatchConnectionManager let cameraDevice: String let mirroringDevice: String + let remoteType: DeviceType? private var heartbeatTask: Task? init( _ list: ConnectionList, - _ browser: Browser + _ browser: Browser, + _ watchConnectionManager: WatchConnectionManager ) { self.cameraDevice = list.cameraName self.mirroringDevice = list.mirroringName + self.remoteType = list.remoteType self.browser = browser + self.watchConnectionManager = watchConnectionManager Task { @MainActor in reduce(.setRemoteDevice(list.remoteName)) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift index 1ec83b31..a150bf15 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift @@ -11,8 +11,8 @@ struct ConnectionCheckView: View { @Environment(Router.self) var router: Router @State private var store: ConnectionCheckStore - init(_ list: ConnectionList, browser: Browser) { - self.store = .init(list, browser) + init(_ list: ConnectionList, browser: Browser, watchConnectionManager: WatchConnectionManager) { + self.store = .init(list, browser, watchConnectionManager) } var body: some View { @@ -86,6 +86,8 @@ struct ConnectionCheckView: View { CameraPreview( store.browser, mirroringName: store.mirroringDevice, + remoteType: store.remoteType, + watchConnectionManager: store.watchConnectionManager, onDismissByCaptureCompletion: { store.send(.navigateToCompletion(true)) } diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift index 649e680b..1eab63f8 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift @@ -19,9 +19,17 @@ struct CameraPreview: View { init( _ browser: Browser, mirroringName: String, + remoteType: DeviceType?, + watchConnectionManager: WatchConnectionManager?, onDismissByCaptureCompletion: (() -> Void)? = nil ) { - self.store = .init(browser: browser, manager: CameraManager(), deviceName: mirroringName) + self.store = .init( + browser: browser, + manager: CameraManager(), + deviceName: mirroringName, + remoteType: remoteType, + watchConnectionManager: watchConnectionManager + ) self.onDismissByCaptureCompletion = onDismissByCaptureCompletion } @@ -93,7 +101,7 @@ struct CameraPreview: View { } .homeAlert( isPresented: Binding( - get: { store.state.isMirroringDisconnected }, + get: { store.state.isPrimaryDeviceDisconnected }, set: { _ in } ), message: "기기 연결이 끊겼습니다.", diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift index ed34ad64..dda06690 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift @@ -21,14 +21,14 @@ final class CameraPreviewStore: StoreProtocol { var animationFlag: Bool = false var isTransferring: Bool = false var isCaptureCompleted: Bool = false - var isMirroringDisconnected: Bool = false + var isPrimaryDeviceDisconnected: Bool = false } enum Intent { case entry(withAngle: Int) case exit case updateAngle(rawValue: Int) - case isMirroringDisconnected + case isPrimaryDeviceDisconnected case browserEvent(CameraStreamEvents) } @@ -41,12 +41,14 @@ final class CameraPreviewStore: StoreProtocol { case setIsTransferring(Bool) case captureCompleted case resetCaptureCompleted - case isMirroringDisconnected + case setIsPrimaryDeviceDisconnected } private(set) var state: State private let browser: Browser private let cameraManager: CameraManageable + private let remoteType: DeviceType? + private let watchConnectionManager: WatchConnectionManager? private var cancellables = Set() private var heartbeatTask: Task? @@ -58,12 +60,17 @@ final class CameraPreviewStore: StoreProtocol { browser: Browser, manager: CameraManageable, deviceName: String, + remoteType: DeviceType?, + watchConnectionManager: WatchConnectionManager? ) { self.browser = browser self.cameraManager = manager + self.remoteType = remoteType + self.watchConnectionManager = watchConnectionManager self.state = State(deviceName: deviceName) setupHeartbeatListener() + setupWatchConnectionListener() } deinit { @@ -89,8 +96,8 @@ final class CameraPreviewStore: StoreProtocol { case .updateAngle(let rawValue): return [.updateAngle(rawValue)] - case .isMirroringDisconnected: - return [.isMirroringDisconnected] + case .isPrimaryDeviceDisconnected: + return [.setIsPrimaryDeviceDisconnected] case .browserEvent(let event): return handleBrowserEvent(event) @@ -122,8 +129,8 @@ final class CameraPreviewStore: StoreProtocol { case .resetCaptureCompleted: state.isCaptureCompleted = false - case .isMirroringDisconnected: - state.isMirroringDisconnected = true + case .setIsPrimaryDeviceDisconnected: + state.isPrimaryDeviceDisconnected = true } self.state = state } @@ -187,16 +194,37 @@ extension CameraPreviewStore { await MainActor.run { switch event { case .heartbeatTimeout: - self.send(.isMirroringDisconnected) + self.send(.isPrimaryDeviceDisconnected) self.browser.disconnect(useType: .remote) case .remoteHeartbeatTimeout: - self.browser.sendCommand(.switchSelectModeView) + // 타이머 모드면 리모트 감지 안 함 + guard self.remoteType != nil else { return } + self.handleDisconnection() } } } } } + private func setupWatchConnectionListener() { + // 타이머 모드면 리모트 감지 안 함 + guard remoteType == .watch else { return } + + watchConnectionManager?.onReachableChanged = { [weak self] isReachable in + guard let self else { return } + Task { @MainActor in + if !isReachable { + self.handleDisconnection() + } + } + } + } + + private func handleDisconnection() { + send(.isPrimaryDeviceDisconnected) + browser.disconnect() + } + private func handleBrowserEvent(_ event: CameraStreamEvents) -> [Result] { switch event { case .sendPhoto: From 690f152c1ed348ae86c8ad51b58e0ca056bab725 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 13:40:42 +0900 Subject: [PATCH 08/17] =?UTF-8?q?fix:=20=EC=B4=AC=EC=98=81=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=AC=EB=8A=94=20=EC=8B=9C=EC=A0=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Device/Mirroring/ModeSettings/ModeSelectionView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift b/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift index 25b08cb8..5e61aa2a 100644 --- a/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift +++ b/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift @@ -108,6 +108,7 @@ struct ModeSelectionView: View { title: "리모콘 모드", description: "나의 Apple Watch에서 \n직접 셔터를 누르세요." ) { + noticeShootingMode(isTimer: false) router.push(to: MirroringRoute.poseSuggestionSelection(isTimerMode: false)) } .disabled(!store.flag) From 234a3e063926b80323e3fd55577121db620073b2 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 13:41:08 +0900 Subject: [PATCH 09/17] =?UTF-8?q?fix:=20=EC=9B=8C=EC=B9=98=EC=97=90?= =?UTF-8?q?=EC=84=9C=20x=20=EB=B2=84=ED=8A=BC=20=EB=88=8C=EB=A0=80?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=20=EC=97=B0=EA=B2=B0=20=ED=95=B4=EC=A0=9C?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Remote/AppleWatch/WatchConnectionManger+watchOS.swift | 8 ++++++++ .../Device/Remote/AppleWatch/WatchConnectionStore.swift | 1 + 2 files changed, 9 insertions(+) diff --git a/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionManger+watchOS.swift b/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionManger+watchOS.swift index cec04f06..da5bc2a0 100644 --- a/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionManger+watchOS.swift +++ b/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionManger+watchOS.swift @@ -158,6 +158,14 @@ final class WatchConnectionManager: NSObject { ) } + /// 사용자가 X 버튼을 눌러 연결을 해제했음을 iPhone에 알립니다. + func sendDisconnectRequest() { + self.sendMessage( + action: .disconnect, + rejectedActionString: "연결 해제 요청을 보낼 수 없습니다." + ) + } + private nonisolated func handleAppStateUpdate(_ applicationContext: [String: Any]) { let appStateRawValue = applicationContext[MessageKey.appState.rawValue] as? String let appStateValue = appStateRawValue.flatMap { AppStateValue(rawValue: $0) } diff --git a/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionStore.swift b/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionStore.swift index 7a194c66..a3b1976b 100644 --- a/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Remote/AppleWatch/WatchConnectionStore.swift @@ -78,6 +78,7 @@ final class WatchConnectionStore: StoreProtocol { self.connectionManager.start() case .disconnect: + self.connectionManager.sendDisconnectRequest() self.connectionManager.stop() return [.setConnectionState(.notConnected)] From ef91d20452806fee14ae8b638142cc20e831a358 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 13:43:33 +0900 Subject: [PATCH 10/17] =?UTF-8?q?fix:=20=EC=B4=AC=EC=98=81=20=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0=EC=97=90=EC=84=9C=20=EC=9B=8C=EC=B9=98=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B3=B4=EB=82=B8=20=EC=97=B0=EA=B2=B0=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=20=EC=9A=94=EC=B2=AD=20=EB=B0=98=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Camera/Browser/WatchConnectionManager+iOS.swift | 6 ++++++ .../Device/Camera/Streaming/CameraPreviewStore.swift | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/WatchConnectionManager+iOS.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/WatchConnectionManager+iOS.swift index d4b2b45e..9ff2c4a7 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/WatchConnectionManager+iOS.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/WatchConnectionManager+iOS.swift @@ -40,6 +40,7 @@ final class WatchConnectionManager: NSObject { var onReachableChanged: ((Bool) -> Void)? var onReceiveCaptureRequest: (() -> Void)? var onReceiveConnectionAck: (() -> Void)? + var onWatchDisconnected: (() -> Void)? override init() { if WCSession.isSupported() { @@ -239,6 +240,11 @@ extension WatchConnectionManager: WCSessionDelegate { self.prepareWatchToCapture() } } + } else if actionValue == ActionValue.disconnect.rawValue { + self.logger.info("워치에서 연결 해제 요청 수신됨.") + Task { @MainActor in + self.onWatchDisconnected?() + } } } diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift index dda06690..7a99da90 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift @@ -210,12 +210,12 @@ extension CameraPreviewStore { // 타이머 모드면 리모트 감지 안 함 guard remoteType == .watch else { return } - watchConnectionManager?.onReachableChanged = { [weak self] isReachable in + watchConnectionManager?.onWatchDisconnected = { [weak self] in guard let self else { return } Task { @MainActor in - if !isReachable { - self.handleDisconnection() - } + // 타이머 모드가 선택되었으면 워치 disconnect 무시 + guard !self.browser.isTimerModeSelected else { return } + self.handleDisconnection() } } } From 47b2b3980a28153eaef6b5d7fd07bdb4a1956126 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 13:46:09 +0900 Subject: [PATCH 11/17] =?UTF-8?q?fix:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EC=83=81=ED=83=9C=20browser=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B8=B0=EB=A1=9D=20->=20=EB=8A=A6=EA=B2=8C=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=EB=B0=9B=EB=8A=94=20=EC=83=81=ED=99=A9=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mirroringBooth/Device/Camera/Browser/Browser.swift | 3 +++ .../Device/Camera/Browser/Manager/BrowserCommandManager.swift | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/Browser.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/Browser.swift index 7c6b3e69..71ed3e3f 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/Browser.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/Browser.swift @@ -45,6 +45,9 @@ final class Browser: NSObject, BrowserCommandDelegate { /// 타이머 모드 선택 명령 수신 콜백 var onSelectedTimerModeCommand: (() -> Void)? + /// 타이머 모드가 선택되었는지 여부 (워치 disconnect 무시용) + var isTimerModeSelected: Bool = false + // MARK: - Stream Manager let streamManager = BrowserStreamManager() diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/Manager/BrowserCommandManager.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/Manager/BrowserCommandManager.swift index 30ae52f7..e2cd8e49 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/Manager/BrowserCommandManager.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/Manager/BrowserCommandManager.swift @@ -15,6 +15,7 @@ protocol BrowserCommandDelegate: AnyObject { var onSelectedTimerModeCommand: (() -> Void)? { get } var mirroringHeartBeater: HeartBeater { get } var remoteHeartBeater: HeartBeater? { get } + var isTimerModeSelected: Bool { get set } } /// 명령 수신 및 실행을 담당하는 매니저 @@ -42,11 +43,14 @@ final class BrowserCommandManager { self?.delegate?.sendRemoteCommand(.navigateToRemoteComplete) } case .setRemoteMode: + delegate?.isTimerModeSelected = false DispatchQueue.main.async { [weak self] in self?.delegate?.onRemoteModeCommand?() self?.delegate?.sendRemoteCommand(.navigateToRemoteCapture) } case .selectedTimerMode: + // 워치 disconnect 무시를 위해 동기적으로 먼저 플래그 설정 + delegate?.isTimerModeSelected = true DispatchQueue.main.async { [weak self] in self?.delegate?.onSelectedTimerModeCommand?() self?.delegate?.sendRemoteCommand(.navigateToHome) From 6a0dde0ff9a8be47bff2be309dbe13dc64fb7892 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 14:00:20 +0900 Subject: [PATCH 12/17] =?UTF-8?q?style:=20=ED=95=84=EC=9A=94=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Device/Camera/Streaming/CameraPreview.swift | 1 - .../Device/Camera/Streaming/CameraPreviewStore.swift | 8 -------- 2 files changed, 9 deletions(-) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift index 1eab63f8..3abb2a23 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift @@ -27,7 +27,6 @@ struct CameraPreview: View { browser: browser, manager: CameraManager(), deviceName: mirroringName, - remoteType: remoteType, watchConnectionManager: watchConnectionManager ) self.onDismissByCaptureCompletion = onDismissByCaptureCompletion diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift index 7a99da90..d53e2630 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift @@ -47,7 +47,6 @@ final class CameraPreviewStore: StoreProtocol { private(set) var state: State private let browser: Browser private let cameraManager: CameraManageable - private let remoteType: DeviceType? private let watchConnectionManager: WatchConnectionManager? private var cancellables = Set() private var heartbeatTask: Task? @@ -60,12 +59,10 @@ final class CameraPreviewStore: StoreProtocol { browser: Browser, manager: CameraManageable, deviceName: String, - remoteType: DeviceType?, watchConnectionManager: WatchConnectionManager? ) { self.browser = browser self.cameraManager = manager - self.remoteType = remoteType self.watchConnectionManager = watchConnectionManager self.state = State(deviceName: deviceName) @@ -197,8 +194,6 @@ extension CameraPreviewStore { self.send(.isPrimaryDeviceDisconnected) self.browser.disconnect(useType: .remote) case .remoteHeartbeatTimeout: - // 타이머 모드면 리모트 감지 안 함 - guard self.remoteType != nil else { return } self.handleDisconnection() } } @@ -207,9 +202,6 @@ extension CameraPreviewStore { } private func setupWatchConnectionListener() { - // 타이머 모드면 리모트 감지 안 함 - guard remoteType == .watch else { return } - watchConnectionManager?.onWatchDisconnected = { [weak self] in guard let self else { return } Task { @MainActor in From 914846bd00cfacd44543dfecb7ac5230669dfb52 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 14:00:39 +0900 Subject: [PATCH 13/17] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=AA=A8=ED=8A=B8?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20=ED=99=94=EB=A9=B4=EC=9D=98=20=EB=B0=B1?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mirroringBooth/Device/Remote/RemoteCaptureView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/mirroringBooth/mirroringBooth/Device/Remote/RemoteCaptureView.swift b/mirroringBooth/mirroringBooth/Device/Remote/RemoteCaptureView.swift index 787f4d54..175376c9 100644 --- a/mirroringBooth/mirroringBooth/Device/Remote/RemoteCaptureView.swift +++ b/mirroringBooth/mirroringBooth/Device/Remote/RemoteCaptureView.swift @@ -28,5 +28,6 @@ struct RemoteCaptureView: View { } } } + .navigationBarBackButtonHidden() } } From 7202145e146a425ffc272aea2285a88ac211f304 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 16:34:24 +0900 Subject: [PATCH 14/17] =?UTF-8?q?style:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=86=8D=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mirroringBooth/WatchStoreTests/WatchStoreTests.swift | 2 +- mirroringBooth/mirroringBooth/Core/Router.swift | 1 - .../mirroringBooth/Device/Camera/Browser/BrowsingView.swift | 3 +-- .../Device/Camera/ConnectionCheck/ConnectionCheckStore.swift | 2 -- .../Device/Camera/ConnectionCheck/ConnectionCheckView.swift | 1 - .../mirroringBooth/Device/Camera/Streaming/CameraPreview.swift | 1 - 6 files changed, 2 insertions(+), 8 deletions(-) diff --git a/mirroringBooth/WatchStoreTests/WatchStoreTests.swift b/mirroringBooth/WatchStoreTests/WatchStoreTests.swift index 8912beac..e02f2745 100644 --- a/mirroringBooth/WatchStoreTests/WatchStoreTests.swift +++ b/mirroringBooth/WatchStoreTests/WatchStoreTests.swift @@ -101,7 +101,7 @@ struct WatchStoreTests { @Test func 대기_화면이_나타나면_대기_상태가_켜짐() { let store = makeSUT() - store.send(.setIsWaiting(true)) + store.send(.isWaiting(true)) #expect(store.state.isWaiting == true) } diff --git a/mirroringBooth/mirroringBooth/Core/Router.swift b/mirroringBooth/mirroringBooth/Core/Router.swift index 8cdb786b..8559c5cd 100644 --- a/mirroringBooth/mirroringBooth/Core/Router.swift +++ b/mirroringBooth/mirroringBooth/Core/Router.swift @@ -35,7 +35,6 @@ struct ConnectionList: Hashable { let cameraName: String let mirroringName: String let remoteName: String? - let remoteType: DeviceType? } enum MirroringRoute: Hashable { diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift index 9e2a9ff2..90822518 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Browser/BrowsingView.swift @@ -93,8 +93,7 @@ struct BrowsingView: View { ConnectionList( cameraName: store.browser.myDeviceName, mirroringName: mirroringDevice.id, - remoteName: store.state.remoteDevice?.id, - remoteType: store.state.remoteDevice?.type + remoteName: store.state.remoteDevice?.id ), store.browser, store.watchConnectionManager diff --git a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift index 0554773b..b33459a5 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckStore.swift @@ -39,7 +39,6 @@ final class ConnectionCheckStore: StoreProtocol { let watchConnectionManager: WatchConnectionManager let cameraDevice: String let mirroringDevice: String - let remoteType: DeviceType? private var heartbeatTask: Task? @@ -50,7 +49,6 @@ final class ConnectionCheckStore: StoreProtocol { ) { self.cameraDevice = list.cameraName self.mirroringDevice = list.mirroringName - self.remoteType = list.remoteType self.browser = browser self.watchConnectionManager = watchConnectionManager diff --git a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift index a150bf15..a9c2839d 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/ConnectionCheck/ConnectionCheckView.swift @@ -86,7 +86,6 @@ struct ConnectionCheckView: View { CameraPreview( store.browser, mirroringName: store.mirroringDevice, - remoteType: store.remoteType, watchConnectionManager: store.watchConnectionManager, onDismissByCaptureCompletion: { store.send(.navigateToCompletion(true)) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift index 3abb2a23..8b87d007 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreview.swift @@ -19,7 +19,6 @@ struct CameraPreview: View { init( _ browser: Browser, mirroringName: String, - remoteType: DeviceType?, watchConnectionManager: WatchConnectionManager?, onDismissByCaptureCompletion: (() -> Void)? = nil ) { From b1eb8ea74cc0c636e4d725960182ffb067cf180f Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 16:34:53 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=84=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EB=AA=A8=EB=93=A0=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Device/Camera/Streaming/CameraPreviewStore.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift index d53e2630..1446a394 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift @@ -191,8 +191,7 @@ extension CameraPreviewStore { await MainActor.run { switch event { case .heartbeatTimeout: - self.send(.isPrimaryDeviceDisconnected) - self.browser.disconnect(useType: .remote) + self.handleDisconnection() case .remoteHeartbeatTimeout: self.handleDisconnection() } From d3ffbcfab4f5d2448a2aef9445d0acfdc201ed5e Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 19:59:06 +0900 Subject: [PATCH 16/17] =?UTF-8?q?fix:=20=EB=B3=91=ED=95=A9=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mirroringBooth/StoreTests/CameraPreviewStoreTests.swift | 7 ++++--- .../Device/Mirroring/ModeSettings/ModeSelectionView.swift | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mirroringBooth/StoreTests/CameraPreviewStoreTests.swift b/mirroringBooth/StoreTests/CameraPreviewStoreTests.swift index c0b06df6..f60a60d8 100644 --- a/mirroringBooth/StoreTests/CameraPreviewStoreTests.swift +++ b/mirroringBooth/StoreTests/CameraPreviewStoreTests.swift @@ -47,7 +47,8 @@ struct CameraPreviewStoreTests { let store = CameraPreviewStore( browser: Browser(), manager: manager, - deviceName: deviceName + deviceName: deviceName, + watchConnectionManager: nil ) return (store, manager) } @@ -154,9 +155,9 @@ struct CameraPreviewStoreTests { @Test func 미러링_연결이_끊기면_상태가_반영됨() { let (store, _) = makeSUT() - store.send(.isMirroringDisconnected) + store.send(.isPrimaryDeviceDisconnected) - #expect(store.state.isMirroringDisconnected == true) + #expect(store.state.isPrimaryDeviceDisconnected == true) } // MARK: - reduce 테스트 (Browser 관련 Intent 대체) diff --git a/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift b/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift index 5e61aa2a..25b08cb8 100644 --- a/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift +++ b/mirroringBooth/mirroringBooth/Device/Mirroring/ModeSettings/ModeSelectionView.swift @@ -108,7 +108,6 @@ struct ModeSelectionView: View { title: "리모콘 모드", description: "나의 Apple Watch에서 \n직접 셔터를 누르세요." ) { - noticeShootingMode(isTimer: false) router.push(to: MirroringRoute.poseSuggestionSelection(isTimerMode: false)) } .disabled(!store.flag) From 94b8b8ff2bcf3016c31ba50e759c2e76f54a416f Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Thu, 5 Feb 2026 20:03:55 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix:=20=EA=B9=83=ED=97=99=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(main=20a?= =?UTF-8?q?ctor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mirroringBooth/mirroringBooth/App/RootStore.swift | 2 +- .../Device/Camera/Streaming/CameraPreviewStore.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mirroringBooth/mirroringBooth/App/RootStore.swift b/mirroringBooth/mirroringBooth/App/RootStore.swift index a10170ec..df004195 100644 --- a/mirroringBooth/mirroringBooth/App/RootStore.swift +++ b/mirroringBooth/mirroringBooth/App/RootStore.swift @@ -70,7 +70,7 @@ extension RootStore { for await stream in advertiser.rootStream { switch stream { case .onHeartbeatTimeout: - await MainActor.run { + await MainActor.run { [weak self] in self?.send(.showTimeoutAlert(true)) } } diff --git a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift index 1446a394..244a70b8 100644 --- a/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift +++ b/mirroringBooth/mirroringBooth/Device/Camera/Streaming/CameraPreviewStore.swift @@ -211,6 +211,7 @@ extension CameraPreviewStore { } } + @MainActor private func handleDisconnection() { send(.isPrimaryDeviceDisconnected) browser.disconnect()