feat(live-room): 라이브 캡쳐 녹화 가능 여부를 생성 조회에 반영한다

This commit is contained in:
2026-03-30 21:27:40 +09:00
parent a4ffab0351
commit 38fd826fe4
7 changed files with 114 additions and 3 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE live_room
ADD COLUMN is_capture_recording_available TINYINT(1) NOT NULL DEFAULT 0 COMMENT '캡쳐/녹화 가능 여부';

View File

@@ -0,0 +1,20 @@
# 라이브 캡쳐/녹화 설정 추가
## 구현 항목
- [x] 라이브 생성/수정/조회 관련 기존 필드 및 흐름 분석
- [x] 라이브 정보에 캡쳐/녹화 단일 가능 여부 플래그 추가
- [x] 라이브 생성 시에만 캡쳐/녹화 가능 여부를 설정하도록 반영
- [x] DB 컬럼 추가 DDL 작성
- [x] 관련 테스트 코드 보강
- [x] 정적 진단/테스트/빌드 검증 수행
## 검증 기록
### 1차 구현
- 무엇을: 라이브 생성 요청(`CreateLiveRoomRequest`)과 라이브 엔티티(`LiveRoom`)에 `isCaptureRecordingAvailable` 단일 플래그를 추가하고, 라이브 정보 응답(`GetRoomInfoResponse`)에 동일 플래그를 노출하도록 반영했다.
- 왜: 캡쳐/녹화를 분리하지 않고 하나의 설정값으로 관리하면서, 해당 값이 생성 시점에만 결정되도록 하기 위해서다.
- 어떻게:
- `./gradlew test --tests "kr.co.vividnext.sodalive.live.room.LiveRoomServiceAdultVisibilityPolicyTest"` 실행 결과: 성공
- `./gradlew build` 실행 결과: 성공
- 수동 QA(서비스 단위): `shouldPersistCaptureAndRecordingAvailabilityOnCreate`, `shouldIncludeCaptureAndRecordingAvailabilityInRoomInfo` 테스트로 생성 저장값/정보 응답값 확인
- `lsp_diagnostics` 실행 결과: `.kt` LSP 서버 미구성으로 실행 불가(대신 Gradle 컴파일·ktlint·test·build 통과로 검증)

View File

@@ -16,5 +16,6 @@ data class CreateLiveRoomRequest(
val menuPan: String = "",
val isActiveMenuPan: Boolean = false,
val isAvailableJoinCreator: Boolean = true,
val genderRestriction: GenderRestriction = GenderRestriction.ALL
val genderRestriction: GenderRestriction = GenderRestriction.ALL,
val isCaptureRecordingAvailable: Boolean = false
)

View File

@@ -34,7 +34,8 @@ data class LiveRoom(
@Column(nullable = true)
var password: String? = null,
@Enumerated(value = EnumType.STRING)
var genderRestriction: GenderRestriction = GenderRestriction.ALL
var genderRestriction: GenderRestriction = GenderRestriction.ALL,
val isCaptureRecordingAvailable: Boolean = false
) : BaseEntity() {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)

View File

@@ -427,7 +427,8 @@ class LiveRoomService(
type = request.type,
password = request.password,
isAvailableJoinCreator = request.isAvailableJoinCreator,
genderRestriction = request.genderRestriction
genderRestriction = request.genderRestriction,
isCaptureRecordingAvailable = request.isCaptureRecordingAvailable
)
room.member = member
@@ -1066,6 +1067,7 @@ class LiveRoomService(
},
isFollowing = isFollowing,
isAdult = room.isAdult,
isCaptureRecordingAvailable = room.isCaptureRecordingAvailable,
participantsCount = roomInfo.listenerCount + roomInfo.speakerCount + roomInfo.managerCount,
totalAvailableParticipantsCount = room.numberOfPeople,
speakerList = roomInfo.speakerList,

View File

@@ -14,6 +14,7 @@ data class GetRoomInfoResponse(
val creatorProfileUrl: String,
val isFollowing: Boolean,
val isAdult: Boolean,
val isCaptureRecordingAvailable: Boolean,
val participantsCount: Int,
val totalAvailableParticipantsCount: Int,
val speakerList: List<LiveRoomMember>,

View File

@@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
import kr.co.vividnext.sodalive.live.room.kickout.LiveRoomKickOutService
import kr.co.vividnext.sodalive.live.room.menu.LiveRoomMenuService
@@ -33,9 +34,12 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.PageRequest
import java.time.LocalDateTime
import java.util.Optional
class LiveRoomServiceAdultVisibilityPolicyTest {
private lateinit var menuService: LiveRoomMenuService
@@ -240,6 +244,86 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
)
}
@Test
@DisplayName("라이브 생성 시 캡쳐/녹화 가능 여부를 저장한다")
fun shouldPersistCaptureAndRecordingAvailabilityOnCreate() {
val member = createMember(id = 300L)
val request = CreateLiveRoomRequest(
title = "title",
content = "notice",
coverImageUrl = "https://image.example.com/cover.png",
isAdult = false,
tags = emptyList(),
numberOfPeople = 10,
timezone = "Asia/Seoul",
isCaptureRecordingAvailable = false
)
Mockito.`when`(repository.getActiveRoomIdList(memberId = 300L)).thenReturn(0)
Mockito.`when`(repository.getRoomActiveAndChannelNameIsNotNull(memberId = 300L)).thenReturn(emptyList())
Mockito.`when`(objectMapper.readValue("request-json", CreateLiveRoomRequest::class.java)).thenReturn(request)
Mockito.`when`(repository.save(Mockito.any(LiveRoom::class.java))).thenAnswer { invocation ->
invocation.arguments[0] as LiveRoom
}
service.createLiveRoom(coverImage = null, requestString = "request-json", member = member)
val roomCaptor = ArgumentCaptor.forClass(LiveRoom::class.java)
Mockito.verify(repository).save(roomCaptor.capture())
assertEquals(false, roomCaptor.value.isCaptureRecordingAvailable)
}
@Test
@DisplayName("라이브 정보 조회에 캡쳐/녹화 가능 여부를 포함한다")
fun shouldIncludeCaptureAndRecordingAvailabilityInRoomInfo() {
val viewer = createMember(id = 400L)
val creator = createMember(id = 401L)
creator.isVisibleDonationRank = false
val room = LiveRoom(
title = "title",
notice = "notice",
beginDateTime = LocalDateTime.now(),
numberOfPeople = 10,
coverImage = "cover/image.png",
isAdult = false,
isCaptureRecordingAvailable = false
)
room.id = 999L
room.member = creator
room.channelName = "SODA_LIVE_CHANNEL_TEST"
val roomInfo = LiveRoomInfo(roomId = 999L)
Mockito.`when`(roomInfoRepository.findById(999L)).thenReturn(Optional.of(roomInfo))
Mockito.`when`(repository.findById(999L)).thenReturn(Optional.of(room))
Mockito.`when`(blockMemberRepository.isBlocked(Mockito.anyLong(), Mockito.anyLong())).thenReturn(false)
Mockito.`when`(
rtcTokenBuilder.buildTokenWithUid(
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyInt()
)
).thenReturn("rtc-token")
Mockito.`when`(
rtmTokenBuilder.buildToken(
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyInt()
)
).thenReturn("rtm-token")
Mockito.`when`(explorerQueryRepository.getNotificationUserIds(creator.id!!)).thenReturn(emptyList())
Mockito.`when`(rouletteRepository.findByCreatorId(creator.id!!)).thenReturn(emptyList())
Mockito.`when`(pushTokenRepository.findByMemberId(creator.id!!)).thenReturn(emptyList())
val response = service.getRoomInfo(roomId = 999L, member = viewer)
assertEquals(false, response.isCaptureRecordingAvailable)
}
private fun createMember(id: Long): Member {
val member = Member(
email = "member$id@test.com",