feat(live-room): 라이브 캡쳐 녹화 허용 설정을 생성 시청 흐름에 반영한다
This commit is contained in:
@@ -716,6 +716,10 @@ enum I18n {
|
|||||||
static var joinAllowed: String { pick(ko: "가능", en: "Allowed", ja: "可能") }
|
static var joinAllowed: String { pick(ko: "가능", en: "Allowed", ja: "可能") }
|
||||||
static var joinNotAllowed: String { pick(ko: "불가능", en: "Not 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 allAges: String { pick(ko: "전체 연령", en: "All ages", ja: "全年齢") }
|
||||||
static var over19: String { pick(ko: "19세 이상", en: "19+", ja: "R-18") }
|
static var over19: String { pick(ko: "19세 이상", en: "19+", ja: "R-18") }
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ struct CreateLiveRoomRequest: Encodable {
|
|||||||
var menuPan: String = ""
|
var menuPan: String = ""
|
||||||
var isActiveMenuPan: Bool = false
|
var isActiveMenuPan: Bool = false
|
||||||
var isAvailableJoinCreator: Bool = true
|
var isAvailableJoinCreator: Bool = true
|
||||||
|
var isCaptureRecordingAvailable: Bool = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ struct GetRecentRoomInfoResponse: Decodable {
|
|||||||
let coverImagePath: String
|
let coverImagePath: String
|
||||||
let numberOfPeople: Int
|
let numberOfPeople: Int
|
||||||
let genderRestriction: LiveRoomCreateViewModel.GenderRestriction
|
let genderRestriction: LiveRoomCreateViewModel.GenderRestriction
|
||||||
|
let isCaptureRecordingAvailable: Bool?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,32 @@ struct LiveRoomCreateView: View {
|
|||||||
}
|
}
|
||||||
.frame(width: screenSize().width - 26.7)
|
.frame(width: screenSize().width - 26.7)
|
||||||
.padding(.top, 33.3)
|
.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 {
|
if shouldShowAdultSetting {
|
||||||
AdultSettingView()
|
AdultSettingView()
|
||||||
.frame(width: screenSize().width - 26.7)
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ final class LiveRoomCreateViewModel: ObservableObject {
|
|||||||
@Published var selectedMenu: SelectedMenu? = nil
|
@Published var selectedMenu: SelectedMenu? = nil
|
||||||
|
|
||||||
@Published var isAvailableJoinCreator = true
|
@Published var isAvailableJoinCreator = true
|
||||||
|
@Published var isCaptureRecordingAvailable = false
|
||||||
|
|
||||||
private let repository = LiveRepository()
|
private let repository = LiveRepository()
|
||||||
private var subscription = Set<AnyCancellable>()
|
private var subscription = Set<AnyCancellable>()
|
||||||
@@ -146,6 +147,7 @@ final class LiveRoomCreateViewModel: ObservableObject {
|
|||||||
self.coverImagePath = data.coverImagePath
|
self.coverImagePath = data.coverImagePath
|
||||||
self.numberOfPeople = String(data.numberOfPeople)
|
self.numberOfPeople = String(data.numberOfPeople)
|
||||||
self.genderRestriction = data.genderRestriction
|
self.genderRestriction = data.genderRestriction
|
||||||
|
self.isCaptureRecordingAvailable = data.isCaptureRecordingAvailable ?? false
|
||||||
|
|
||||||
self.errorMessage = I18n.CreateLive.recentDataLoaded
|
self.errorMessage = I18n.CreateLive.recentDataLoaded
|
||||||
self.isShowPopup = true
|
self.isShowPopup = true
|
||||||
@@ -192,7 +194,8 @@ final class LiveRoomCreateViewModel: ObservableObject {
|
|||||||
menuPanId: isActivateMenu ? menuId : 0,
|
menuPanId: isActivateMenu ? menuId : 0,
|
||||||
menuPan: isActivateMenu ? menu : "",
|
menuPan: isActivateMenu ? menu : "",
|
||||||
isActiveMenuPan: isActivateMenu,
|
isActiveMenuPan: isActivateMenu,
|
||||||
isAvailableJoinCreator: isAvailableJoinCreator
|
isAvailableJoinCreator: isAvailableJoinCreator,
|
||||||
|
isCaptureRecordingAvailable: isCaptureRecordingAvailable
|
||||||
)
|
)
|
||||||
|
|
||||||
if timeSettingMode == .RESERVATION {
|
if timeSettingMode == .RESERVATION {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ struct GetRoomInfoResponse: Decodable {
|
|||||||
let creatorLanguageCode: String?
|
let creatorLanguageCode: String?
|
||||||
let isActiveRoulette: Bool
|
let isActiveRoulette: Bool
|
||||||
let isChatFrozen: Bool?
|
let isChatFrozen: Bool?
|
||||||
|
let isCaptureRecordingAvailable: Bool?
|
||||||
let isPrivateRoom: Bool
|
let isPrivateRoom: Bool
|
||||||
let password: String?
|
let password: String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,15 @@ struct LiveRoomViewV2: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var shouldEnforceScreenCaptureProtection: Bool {
|
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 {
|
var body: some View {
|
||||||
@@ -956,6 +964,9 @@ struct LiveRoomViewV2: View {
|
|||||||
.onChange(of: viewModel.liveRoomInfo?.managerList) { _ in
|
.onChange(of: viewModel.liveRoomInfo?.managerList) { _ in
|
||||||
syncScreenCaptureProtectionState()
|
syncScreenCaptureProtectionState()
|
||||||
}
|
}
|
||||||
|
.onChange(of: viewModel.liveRoomInfo?.isCaptureRecordingAvailable) { _ in
|
||||||
|
syncScreenCaptureProtectionState()
|
||||||
|
}
|
||||||
.onChange(of: viewModel.isChatFrozenForCurrentUser) { isFrozen in
|
.onChange(of: viewModel.isChatFrozenForCurrentUser) { isFrozen in
|
||||||
if isFrozen {
|
if isFrozen {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
|
|||||||
48
docs/20260330_라이브캡쳐녹화가능여부설정추가.md
Normal file
48
docs/20260330_라이브캡쳐녹화가능여부설정추가.md
Normal file
@@ -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`)
|
||||||
Reference in New Issue
Block a user