Merge pull request 'feat(live-room): 라이브 캡쳐 녹화 가능 여부를 생성 조회에 반영한다' (#414) from test into main
Reviewed-on: #414
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 menuPan: String = "",
|
||||||
val isActiveMenuPan: Boolean = false,
|
val isActiveMenuPan: Boolean = false,
|
||||||
val isAvailableJoinCreator: Boolean = true,
|
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)
|
@Column(nullable = true)
|
||||||
var password: String? = null,
|
var password: String? = null,
|
||||||
@Enumerated(value = EnumType.STRING)
|
@Enumerated(value = EnumType.STRING)
|
||||||
var genderRestriction: GenderRestriction = GenderRestriction.ALL
|
var genderRestriction: GenderRestriction = GenderRestriction.ALL,
|
||||||
|
val isCaptureRecordingAvailable: Boolean = false
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
@OneToOne(fetch = FetchType.LAZY)
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "member_id", nullable = false)
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
|
|||||||
@@ -427,7 +427,8 @@ class LiveRoomService(
|
|||||||
type = request.type,
|
type = request.type,
|
||||||
password = request.password,
|
password = request.password,
|
||||||
isAvailableJoinCreator = request.isAvailableJoinCreator,
|
isAvailableJoinCreator = request.isAvailableJoinCreator,
|
||||||
genderRestriction = request.genderRestriction
|
genderRestriction = request.genderRestriction,
|
||||||
|
isCaptureRecordingAvailable = request.isCaptureRecordingAvailable
|
||||||
)
|
)
|
||||||
room.member = member
|
room.member = member
|
||||||
|
|
||||||
@@ -1066,6 +1067,7 @@ class LiveRoomService(
|
|||||||
},
|
},
|
||||||
isFollowing = isFollowing,
|
isFollowing = isFollowing,
|
||||||
isAdult = room.isAdult,
|
isAdult = room.isAdult,
|
||||||
|
isCaptureRecordingAvailable = room.isCaptureRecordingAvailable,
|
||||||
participantsCount = roomInfo.listenerCount + roomInfo.speakerCount + roomInfo.managerCount,
|
participantsCount = roomInfo.listenerCount + roomInfo.speakerCount + roomInfo.managerCount,
|
||||||
totalAvailableParticipantsCount = room.numberOfPeople,
|
totalAvailableParticipantsCount = room.numberOfPeople,
|
||||||
speakerList = roomInfo.speakerList,
|
speakerList = roomInfo.speakerList,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ data class GetRoomInfoResponse(
|
|||||||
val creatorProfileUrl: String,
|
val creatorProfileUrl: String,
|
||||||
val isFollowing: Boolean,
|
val isFollowing: Boolean,
|
||||||
val isAdult: Boolean,
|
val isAdult: Boolean,
|
||||||
|
val isCaptureRecordingAvailable: Boolean,
|
||||||
val participantsCount: Int,
|
val participantsCount: Int,
|
||||||
val totalAvailableParticipantsCount: Int,
|
val totalAvailableParticipantsCount: Int,
|
||||||
val speakerList: List<LiveRoomMember>,
|
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.i18n.SodaMessageSource
|
||||||
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
||||||
import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository
|
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.info.LiveRoomInfoRedisRepository
|
||||||
import kr.co.vividnext.sodalive.live.room.kickout.LiveRoomKickOutService
|
import kr.co.vividnext.sodalive.live.room.kickout.LiveRoomKickOutService
|
||||||
import kr.co.vividnext.sodalive.live.room.menu.LiveRoomMenuService
|
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.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
class LiveRoomServiceAdultVisibilityPolicyTest {
|
class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||||
private lateinit var menuService: LiveRoomMenuService
|
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 {
|
private fun createMember(id: Long): Member {
|
||||||
val member = Member(
|
val member = Member(
|
||||||
email = "member$id@test.com",
|
email = "member$id@test.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user