diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 5e29764..2774cdd 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -716,6 +716,10 @@ enum I18n { static var joinAllowed: String { pick(ko: "가능", en: "Allowed", ja: "可能") } static var joinNotAllowed: String { pick(ko: "불가능", en: "Not allowed", ja: "不可") } + static var captureRecordingSetting: String { pick(ko: "캡쳐/녹화 허용", en: "Capture/recording", ja: "キャプチャ/録画") } + static var captureRecordingAllowed: String { pick(ko: "가능", en: "Allowed", ja: "可能") } + static var captureRecordingNotAllowed: String { pick(ko: "불가능", en: "Not allowed", ja: "不可") } + // 연령 제한 static var allAges: String { pick(ko: "전체 연령", en: "All ages", ja: "全年齢") } static var over19: String { pick(ko: "19세 이상", en: "19+", ja: "R-18") } diff --git a/SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift b/SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift index 6903bb9..e52649f 100644 --- a/SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift +++ b/SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift @@ -24,4 +24,5 @@ struct CreateLiveRoomRequest: Encodable { var menuPan: String = "" var isActiveMenuPan: Bool = false var isAvailableJoinCreator: Bool = true + var isCaptureRecordingAvailable: Bool = false } diff --git a/SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift b/SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift index b2032f4..24d6b0d 100644 --- a/SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift +++ b/SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift @@ -14,4 +14,5 @@ struct GetRecentRoomInfoResponse: Decodable { let coverImagePath: String let numberOfPeople: Int let genderRestriction: LiveRoomCreateViewModel.GenderRestriction + let isCaptureRecordingAvailable: Bool? } diff --git a/SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift index 3b1a206..28c434b 100644 --- a/SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift +++ b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift @@ -182,7 +182,32 @@ struct LiveRoomCreateView: View { } .frame(width: screenSize().width - 26.7) .padding(.top, 33.3) - + + VStack(spacing: 13.3) { + Text(I18n.CreateLive.captureRecordingSetting) + .appFont(size: 16.7, weight: .bold) + .foregroundColor(Color.grayee) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + HStack(spacing: 13.3) { + SelectedButtonView( + title: I18n.CreateLive.captureRecordingAllowed, + isActive: true, + isSelected: viewModel.isCaptureRecordingAvailable + ) + .onTapGesture { viewModel.isCaptureRecordingAvailable = true } + + SelectedButtonView( + title: I18n.CreateLive.captureRecordingNotAllowed, + isActive: true, + isSelected: !viewModel.isCaptureRecordingAvailable + ) + .onTapGesture { viewModel.isCaptureRecordingAvailable = false } + } + } + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + if shouldShowAdultSetting { AdultSettingView() .frame(width: screenSize().width - 26.7) diff --git a/SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift index 112fa94..875abf9 100644 --- a/SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift +++ b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift @@ -100,6 +100,7 @@ final class LiveRoomCreateViewModel: ObservableObject { @Published var selectedMenu: SelectedMenu? = nil @Published var isAvailableJoinCreator = true + @Published var isCaptureRecordingAvailable = false private let repository = LiveRepository() private var subscription = Set() @@ -146,6 +147,7 @@ final class LiveRoomCreateViewModel: ObservableObject { self.coverImagePath = data.coverImagePath self.numberOfPeople = String(data.numberOfPeople) self.genderRestriction = data.genderRestriction + self.isCaptureRecordingAvailable = data.isCaptureRecordingAvailable ?? false self.errorMessage = I18n.CreateLive.recentDataLoaded self.isShowPopup = true @@ -192,7 +194,8 @@ final class LiveRoomCreateViewModel: ObservableObject { menuPanId: isActivateMenu ? menuId : 0, menuPan: isActivateMenu ? menu : "", isActiveMenuPan: isActivateMenu, - isAvailableJoinCreator: isAvailableJoinCreator + isAvailableJoinCreator: isAvailableJoinCreator, + isCaptureRecordingAvailable: isCaptureRecordingAvailable ) if timeSettingMode == .RESERVATION { diff --git a/SodaLive/Sources/Live/Room/GetRoomInfoResponse.swift b/SodaLive/Sources/Live/Room/GetRoomInfoResponse.swift index 96e30dc..0c9a801 100644 --- a/SodaLive/Sources/Live/Room/GetRoomInfoResponse.swift +++ b/SodaLive/Sources/Live/Room/GetRoomInfoResponse.swift @@ -29,6 +29,7 @@ struct GetRoomInfoResponse: Decodable { let creatorLanguageCode: String? let isActiveRoulette: Bool let isChatFrozen: Bool? + let isCaptureRecordingAvailable: Bool? let isPrivateRoom: Bool let password: String? } diff --git a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift index 95b5585..7236455 100644 --- a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift +++ b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift @@ -80,7 +80,15 @@ struct LiveRoomViewV2: View { } private var shouldEnforceScreenCaptureProtection: Bool { - !(isCurrentUserHost || isCurrentUserStaff) + guard let liveRoomInfo = viewModel.liveRoomInfo else { + return true + } + + if liveRoomInfo.isCaptureRecordingAvailable == true { + return false + } + + return !(isCurrentUserHost || isCurrentUserStaff) } var body: some View { @@ -956,6 +964,9 @@ struct LiveRoomViewV2: View { .onChange(of: viewModel.liveRoomInfo?.managerList) { _ in syncScreenCaptureProtectionState() } + .onChange(of: viewModel.liveRoomInfo?.isCaptureRecordingAvailable) { _ in + syncScreenCaptureProtectionState() + } .onChange(of: viewModel.isChatFrozenForCurrentUser) { isFrozen in if isFrozen { hideKeyboard() diff --git a/docs/20260330_라이브캡쳐녹화가능여부설정추가.md b/docs/20260330_라이브캡쳐녹화가능여부설정추가.md new file mode 100644 index 0000000..38bea70 --- /dev/null +++ b/docs/20260330_라이브캡쳐녹화가능여부설정추가.md @@ -0,0 +1,48 @@ +# 20260330 라이브 캡쳐/녹화 가능 여부 설정 추가 + +## 작업 체크리스트 +- [x] 라이브 정보 응답 모델에 `isCaptureRecordingAvailable` 필드 추가 및 매핑 확인 +- [x] `LiveRoomViewV2` 캡쳐/녹화 보호 조건에 라이브 설정값 반영 +- [x] 캡쳐/녹화 불가 라이브에서 방장/스탭 예외 허용 유지 +- [x] 라이브 생성 경로에만 설정값 전송되도록 반영 +- [x] 라이브 수정(편집) 경로에서 해당 설정 변경 불가 상태 유지 확인 +- [x] 진단/빌드/테스트/수동 QA 수행 + +## 수용 기준 (Acceptance Criteria) +- [x] `GetRoomInfoResponse`(또는 동등 라이브 정보 모델)에 `isCaptureRecordingAvailable`가 존재한다. +- [x] 라이브 설정값이 `true`면 일반 참여자도 캡쳐/녹화 보호가 비활성화된다. +- [x] 라이브 설정값이 `false`면 일반 참여자는 기존 캡쳐/녹화 보호가 유지된다. +- [x] 라이브 설정값이 `false`여도 방장/스탭은 캡쳐/녹화 보호 대상이 아니다. +- [x] 설정값은 라이브 생성 요청에서만 설정 가능하고, 라이브 수정 요청에서는 변경되지 않는다. +- [x] 변경 파일 `lsp_diagnostics`를 수행했고 `SodaLive`/`SodaLive-dev` Debug build가 성공한다. + +## 검증 기록 +### 1차 검증 (2026-03-30) +- 무엇/왜/어떻게: + - 무엇: 라이브 정보/생성 요청에 `isCaptureRecordingAvailable`를 추가하고, `LiveRoomViewV2`의 캡쳐 보호 조건을 라이브 설정값 + 방장/스탭 예외로 갱신. + - 왜: 캡쳐/녹화 가능 여부를 라이브 생성 시점에만 제어하면서, 비허용 라이브에서도 운영 권한(방장/스탭) 예외를 유지하기 위해. + - 어떻게: 모델(`GetRoomInfoResponse`, `CreateLiveRoomRequest`, `GetRecentRoomInfoResponse`), 생성 UI/ViewModel(`LiveRoomCreateView`, `LiveRoomCreateViewModel`), 보호 로직(`LiveRoomViewV2`)을 최소 수정으로 연결. +- 실행 명령: + - `lsp_diagnostics(filePath: SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift, severity: all)` + - `lsp_diagnostics(filePath: SodaLive/Sources/Live/Room/GetRoomInfoResponse.swift, severity: all)` + - `lsp_diagnostics(filePath: SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift, severity: all)` + - `lsp_diagnostics(filePath: SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift, severity: all)` + - `lsp_diagnostics(filePath: SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift, severity: all)` + - `lsp_diagnostics(filePath: SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift, severity: all)` + - `lsp_diagnostics(filePath: SodaLive/Sources/I18n/I18n.swift, severity: all)` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build && xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test; xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` + - `grep("isCaptureRecordingAvailable", include: *.swift, path: SodaLive/Sources/Live/Room)` + - `grep("captureRecordingSetting|captureRecordingAllowed|captureRecordingNotAllowed", include: *.swift, path: SodaLive/Sources)` + - `grep("isCaptureRecordingAvailable", include: *.swift, path: SodaLive/Sources/Live/Room/Edit)` +- 결과: + - `lsp_diagnostics`: + - `LiveRoomViewV2.swift`, `GetRecentRoomInfoResponse.swift`, `I18n.swift`는 `No diagnostics found` + - 일부 파일(`CreateLiveRoomRequest.swift`, `LiveRoomCreateViewModel.swift`, `LiveRoomCreateView.swift`, `GetRoomInfoResponse.swift`)은 SourceKit 모듈/심볼 해석 한계(`No such module`, `Cannot find type ... in scope`)가 보고됨 + - 빌드: `SodaLive`, `SodaLive-dev` 모두 `** BUILD SUCCEEDED **` + - 테스트: `Scheme SodaLive is not currently configured for the test action.`, `Scheme SodaLive-dev is not currently configured for the test action.` + - 수동 QA(코드 경로): + - 생성 UI에 캡쳐/녹화 허용 토글 추가 확인 (`LiveRoomCreateView`) + - 생성 요청에만 `isCaptureRecordingAvailable` 전송 확인 (`CreateLiveRoomRequest`, `LiveRoomCreateViewModel`) + - 편집 경로에 해당 필드 미존재 확인 (`Live/Room/Edit` grep 결과 없음) + - 라이브룸 보호 분기 확인: `isCaptureRecordingAvailable == true`면 보호 비활성화, `false`면 방장/스탭 예외 외 참여자 보호 유지 (`LiveRoomViewV2`)