feat(live-room): 라이브 캡쳐 녹화 가능 여부를 생성 조회에 반영한다
This commit is contained in:
2
docs/20260330_live_room_capture_recording_ddl.sql
Normal file
2
docs/20260330_live_room_capture_recording_ddl.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE live_room
|
||||
ADD COLUMN is_capture_recording_available TINYINT(1) NOT NULL DEFAULT 0 COMMENT '캡쳐/녹화 가능 여부';
|
||||
20
docs/20260330_라이브캡쳐녹화설정추가.md
Normal file
20
docs/20260330_라이브캡쳐녹화설정추가.md
Normal 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 통과로 검증)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user