라이브 - 방 만들기, 태그 등록, 태그 조회 API 추가

This commit is contained in:
Klaus 2023-07-31 02:04:32 +09:00
parent f1610af6f6
commit 036107d103
18 changed files with 579 additions and 9 deletions

View File

@ -8,10 +8,13 @@ import kr.co.vividnext.sodalive.can.charge.QCharge.charge
import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.can.payment.QPayment.payment import kr.co.vividnext.sodalive.can.payment.QPayment.payment
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
import kr.co.vividnext.sodalive.can.use.UseCan import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@ -23,6 +26,7 @@ interface CanQueryRepository {
fun findAllByStatus(status: CanStatus): List<CanResponse> fun findAllByStatus(status: CanStatus): List<CanResponse>
fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan> fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan>
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge> fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
} }
@Repository @Repository
@ -93,4 +97,18 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
.orderBy(charge.id.desc()) .orderBy(charge.id.desc())
.fetch() .fetch()
} }
override fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan? {
return queryFactory
.selectFrom(useCan)
.innerJoin(useCan.member, member)
.innerJoin(useCan.room, liveRoom)
.where(
member.id.eq(memberId)
.and(liveRoom.id.eq(roomId))
.and(useCan.canUsage.eq(CanUsage.LIVE))
)
.orderBy(useCan.id.desc())
.fetchFirst()
}
} }

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.extensions
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun String.convertLocalDateTime(format: String): LocalDateTime {
val dateTimeFormatter = DateTimeFormatter.ofPattern(format)
return LocalDateTime.parse(this, dateTimeFormatter)
}

View File

@ -4,7 +4,6 @@ import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.Expressions import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.QMember.member
@ -63,7 +62,6 @@ class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) {
.where( .where(
where where
.and(liveRoom.isActive.isTrue) .and(liveRoom.isActive.isTrue)
.and(liveRoom.type.ne(LiveRoomType.SECRET))
.and(liveRoom.channelName.isNotNull) .and(liveRoom.channelName.isNotNull)
.and(liveRoom.channelName.isNotEmpty) .and(liveRoom.channelName.isNotEmpty)
) )

View File

@ -0,0 +1,27 @@
package kr.co.vividnext.sodalive.live.reservation
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Member
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
import javax.persistence.OneToOne
@Entity
data class LiveReservation(
var isActive: Boolean = true
) : BaseEntity() {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "room_id", nullable = true)
var room: LiveRoom? = null
set(value) {
value?.reservations!!.add(this)
field = value
}
}

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.live.room
data class CreateLiveRoomResponse(
val id: Long?,
val channelName: String?
)

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.live.room
data class CreateSudaRoomRequest(
val title: String,
val content: String,
val coverImageUrl: String? = null,
val isAdult: Boolean,
val tags: List<String>,
val numberOfPeople: Int,
val beginDateTimeString: String? = null,
val price: Int = 0,
val timezone: String,
val type: LiveRoomType = LiveRoomType.OPEN,
val password: String? = null
)

View File

@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive.live.room
import kr.co.vividnext.sodalive.can.use.UseCan import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.live.reservation.LiveReservation
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.persistence.CascadeType
import javax.persistence.Column import javax.persistence.Column
import javax.persistence.Entity import javax.persistence.Entity
import javax.persistence.EnumType import javax.persistence.EnumType
@ -33,9 +35,15 @@ data class LiveRoom(
@JoinColumn(name = "member_id", nullable = false) @JoinColumn(name = "member_id", nullable = false)
var member: Member? = null var member: Member? = null
@OneToMany(mappedBy = "room", cascade = [CascadeType.ALL])
var tags: MutableList<LiveRoomTag> = mutableListOf()
@OneToMany(mappedBy = "room", fetch = FetchType.LAZY) @OneToMany(mappedBy = "room", fetch = FetchType.LAZY)
var useCan: MutableList<UseCan> = mutableListOf() var useCan: MutableList<UseCan> = mutableListOf()
@OneToMany(mappedBy = "room", fetch = FetchType.LAZY)
var reservations: MutableList<LiveReservation> = mutableListOf()
var channelName: String? = null var channelName: String? = null
var isActive: Boolean = true var isActive: Boolean = true
} }
@ -45,10 +53,7 @@ enum class LiveRoomType {
OPEN, OPEN,
// 비공개 // 비공개
PRIVATE, PRIVATE
// 비밀방
SECRET
} }
enum class LiveRoomStatus { enum class LiveRoomStatus {

View File

@ -6,9 +6,13 @@ import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController @RestController
@RequestMapping("/live/room") @RequestMapping("/live/room")
@ -26,4 +30,28 @@ class LiveRoomController(private val service: LiveRoomService) {
ApiResponse.ok(service.getRoomList(dateString, status, pageable, member, timezone)) ApiResponse.ok(service.getRoomList(dateString, status, pageable, member, timezone))
} }
@PostMapping
fun createLiveRoom(
@RequestPart("coverImage") coverImage: MultipartFile?,
@RequestPart("request") requestString: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.createLiveRoom(coverImage, requestString, member))
}
@GetMapping("/detail/{id}")
fun getRoomDetail(
@PathVariable id: Long,
@RequestParam timezone: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member != null) {
ApiResponse.ok(service.getRoomDetail(id, member, timezone))
} else {
throw SodaException("로그인 정보를 확인해주세요.")
}
}
} }

View File

@ -5,6 +5,8 @@ import com.querydsl.core.types.Predicate
import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.core.types.dsl.Expressions import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.live.reservation.LiveReservation
import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember
@ -29,6 +31,9 @@ interface LiveRoomQueryRepository {
timezone: String, timezone: String,
isAdult: Boolean isAdult: Boolean
): List<LiveRoom> ): List<LiveRoom>
fun getLiveRoom(id: Long): LiveRoom?
fun getReservationList(roomId: Long): List<LiveReservation>
} }
class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository {
@ -79,7 +84,6 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L
where = where.and(liveRoom.isActive.isTrue) where = where.and(liveRoom.isActive.isTrue)
.and(liveRoom.member.isNotNull) .and(liveRoom.member.isNotNull)
.and(liveRoom.type.ne(LiveRoomType.SECRET))
return queryFactory return queryFactory
.selectFrom(liveRoom) .selectFrom(liveRoom)
@ -98,6 +102,27 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L
.fetch() .fetch()
} }
override fun getLiveRoom(id: Long): LiveRoom? {
return queryFactory
.selectFrom(liveRoom)
.where(
liveRoom.id.eq(id)
.and(liveRoom.isActive.isTrue)
)
.fetchFirst()
}
override fun getReservationList(roomId: Long): List<LiveReservation> {
return queryFactory
.selectFrom(liveReservation)
.innerJoin(liveReservation.room, liveRoom)
.where(
liveRoom.id.eq(roomId)
.and(liveReservation.isActive.isTrue)
)
.fetch()
}
private fun orderByFieldAccountId( private fun orderByFieldAccountId(
memberId: Long, memberId: Long,
status: LiveRoomStatus, status: LiveRoomStatus,

View File

@ -1,11 +1,24 @@
package kr.co.vividnext.sodalive.live.room package kr.co.vividnext.sodalive.live.room
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
import kr.co.vividnext.sodalive.live.tag.LiveTagRepository
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -14,8 +27,15 @@ class LiveRoomService(
private val repository: LiveRoomRepository, private val repository: LiveRoomRepository,
private val roomInfoRepository: LiveRoomInfoRedisRepository, private val roomInfoRepository: LiveRoomInfoRedisRepository,
private val tagRepository: LiveTagRepository,
private val canRepository: CanRepository,
private val objectMapper: ObjectMapper,
private val s3Uploader: S3Uploader,
@Value("\${cloud.aws.s3.bucket}")
private val coverImageBucket: String,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val coverImageHost: String private val cloudFrontHost: String
) { ) {
fun getRoomList( fun getRoomList(
dateString: String?, dateString: String?,
@ -61,7 +81,7 @@ class LiveRoomService(
coverImageUrl = if (it.coverImage!!.startsWith("https://")) { coverImageUrl = if (it.coverImage!!.startsWith("https://")) {
it.coverImage!! it.coverImage!!
} else { } else {
"$coverImageHost/${it.coverImage!!}" "$cloudFrontHost/${it.coverImage!!}"
}, },
isReservation = false, isReservation = false,
isPrivateRoom = it.type == LiveRoomType.PRIVATE isPrivateRoom = it.type == LiveRoomType.PRIVATE
@ -69,4 +89,182 @@ class LiveRoomService(
} }
.toList() .toList()
} }
fun createLiveRoom(coverImage: MultipartFile?, requestString: String, member: Member): CreateLiveRoomResponse {
val request = objectMapper.readValue(requestString, CreateSudaRoomRequest::class.java)
if (request.coverImageUrl == null && coverImage == null) {
throw SodaException("커버이미지를 선택해 주세요.")
}
val now = LocalDateTime.now()
val beginDateTime = if (request.beginDateTimeString != null) {
request.beginDateTimeString.convertLocalDateTime("yyyy-MM-dd HH:mm")
.atZone(ZoneId.of(request.timezone))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
} else {
now
}
if (
request.beginDateTimeString != null &&
beginDateTime < now.plusMinutes(30)
) {
throw SodaException("현재시각 기준, 30분 이후부터 설정가능합니다.")
}
if (
request.type == LiveRoomType.PRIVATE &&
(request.password == null || request.password.length != 6)
) {
throw SodaException("방 입장 비밀번호 6자리를 입력해 주세요.")
}
val room = LiveRoom(
title = request.title,
notice = request.content,
beginDateTime = beginDateTime,
numberOfPeople = request.numberOfPeople,
isAdult = request.isAdult,
price = request.price,
type = request.type,
password = request.password
)
room.member = member
if (request.beginDateTimeString == null) {
room.channelName = "SODA_LIVE_CHANNEL_" +
"${member.id!!}_${beginDateTime.year}_${beginDateTime.month}_${beginDateTime.dayOfMonth}_" +
"${beginDateTime.hour}_${beginDateTime.minute}"
}
request.tags.forEach {
val tag = tagRepository.findByTag(it)
if (tag != null) {
room.tags.add(LiveRoomTag(room, tag))
if (tag.tag.contains("음담패설")) {
room.isAdult = true
}
}
}
val createdRoom = repository.save(room)
// 이미지 업로드
if (coverImage != null) {
val metadata = ObjectMetadata()
metadata.contentLength = coverImage.size
// 커버 이미지 파일명 생성
val coverImageFileName = generateFileName(prefix = "${room.id}-cover")
// 커버 이미지 업로드
val coverImagePath = s3Uploader.upload(
inputStream = coverImage.inputStream,
bucket = coverImageBucket,
filePath = "live_room_cover/${room.id}/$coverImageFileName",
metadata = metadata
)
room.coverImage = coverImagePath
room.bgImage = coverImagePath
} else {
room.coverImage = request.coverImageUrl
room.bgImage = request.coverImageUrl
}
return CreateLiveRoomResponse(createdRoom.id, createdRoom.channelName)
}
fun getRoomDetail(roomId: Long, member: Member, timezone: String): GetRoomDetailResponse {
val room = repository.getLiveRoom(id = roomId)
?: throw SodaException("이미 종료된 방입니다")
if (room.isAdult && member.auth == null) {
throw SodaException("본인인증이 필요한 서비스 입니다.\n요즘라이브 마이페이지에서 본인인증 후 다시 이용해 주세요.")
}
val beginDateTime = room.beginDateTime
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(timezone))
val response = GetRoomDetailResponse(
roomId = roomId,
title = room.title,
content = room.notice,
price = room.price,
tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(),
numberOfParticipantsTotal = room.numberOfPeople,
numberOfParticipants = 0,
channelName = room.channelName,
beginDateTime = beginDateTime.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")),
isPaid = false,
isPrivateRoom = room.type == LiveRoomType.PRIVATE,
password = room.password
)
response.manager = GetRoomDetailManager(room.member!!, cloudFrontHost = cloudFrontHost)
if (!room.channelName.isNullOrBlank()) {
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
if (roomInfo != null) {
response.isPaid = canRepository.isExistPaidLiveRoom(
memberId = member.id!!,
roomId = roomId
) != null
val users = roomInfo.speakerList
.asSequence()
.map {
GetRoomDetailUser(
it.id,
it.nickname,
"$cloudFrontHost/${it.profileImage}"
)
}.toMutableList()
users.addAll(
roomInfo.listenerList
.asSequence()
.map {
GetRoomDetailUser(
it.id,
it.nickname,
"$cloudFrontHost/${it.profileImage}"
)
}
)
users.addAll(
roomInfo.managerList
.asSequence()
.map {
GetRoomDetailUser(
it.id,
it.nickname,
"$cloudFrontHost/${it.profileImage}"
)
}
)
response.participatingUsers = users
response.numberOfParticipants = users.size
}
} else {
val reservationList = repository.getReservationList(roomId)
response.participatingUsers = reservationList
.asSequence()
.map {
if (it.member!!.id!! == member.id!!) {
response.isPaid = true
}
GetRoomDetailUser(it.member!!, cloudFrontHost)
}
.toList()
response.numberOfParticipants = reservationList.size
}
return response
}
} }

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.live.room
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.live.tag.LiveTag
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
@Entity
class LiveRoomTag(
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "live_room_id", nullable = false)
var room: LiveRoom,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "live_tag_id", nullable = false)
var tag: LiveTag
) : BaseEntity()

View File

@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.live.room.detail
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
data class GetRoomDetailResponse(
val roomId: Long,
val price: Int,
val title: String,
val content: String,
var isPaid: Boolean,
val isPrivateRoom: Boolean,
val password: String?,
val tags: List<String>,
val channelName: String?,
val beginDateTime: String,
var numberOfParticipants: Int,
val numberOfParticipantsTotal: Int
) {
var manager: GetRoomDetailManager? = null
var participatingUsers: List<GetRoomDetailUser> = listOf()
}
data class GetRoomDetailManager(
val id: Long,
val nickname: String,
val introduce: String,
val youtubeUrl: String?,
val instagramUrl: String?,
val websiteUrl: String?,
val blogUrl: String?,
val profileImageUrl: String,
val isCreator: Boolean
) {
constructor(member: Member, cloudFrontHost: String) : this(
id = member.id!!,
nickname = member.nickname,
introduce = member.introduce,
youtubeUrl = member.youtubeUrl,
instagramUrl = member.instagramUrl,
websiteUrl = member.websiteUrl,
blogUrl = member.blogUrl,
profileImageUrl = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
},
isCreator = member.role == MemberRole.CREATOR
)
}
data class GetRoomDetailUser(
val id: Long,
val nickname: String,
val profileImageUrl: String
) {
constructor(member: Member, cloudFrontHost: String) : this(
member.id!!,
member.nickname,
profileImageUrl = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
}
)
}

View File

@ -0,0 +1,3 @@
package kr.co.vividnext.sodalive.live.tag
data class CreateLiveTagRequest(val tag: String)

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.live.tag
import com.querydsl.core.annotations.QueryProjection
data class GetLiveTagResponse @QueryProjection constructor(
val id: Long,
val tag: String,
val image: String
)

View File

@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.live.tag
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.Column
import javax.persistence.Entity
@Entity
data class LiveTag(
@Column(unique = true, nullable = false)
var tag: String,
@Column(nullable = true)
var image: String? = null,
@Column(nullable = false)
var isActive: Boolean = true,
@Column(nullable = false)
var orders: Int = 1
) : BaseEntity()

View File

@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.live.tag
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/live/tag")
class LiveTagController(private val service: LiveTagService) {
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun enrollmentLiveTag(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.enrollmentLiveTag(image, requestString), "등록되었습니다.")
@GetMapping
fun getTags(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
}
ApiResponse.ok(service.getTags(member))
}
}

View File

@ -0,0 +1,40 @@
package kr.co.vividnext.sodalive.live.tag
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.live.tag.QLiveTag.liveTag
import kr.co.vividnext.sodalive.member.MemberRole
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface LiveTagRepository : JpaRepository<LiveTag, Long>, LiveTagQueryRepository {
fun findByTag(it: String): LiveTag?
}
interface LiveTagQueryRepository {
fun getTags(role: MemberRole, isAdult: Boolean, cloudFrontHost: String): List<GetLiveTagResponse>
}
@Repository
class LiveTagQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveTagQueryRepository {
override fun getTags(role: MemberRole, isAdult: Boolean, cloudFrontHost: String): List<GetLiveTagResponse> {
var where = liveTag.isActive.isTrue
if (role != MemberRole.ADMIN && !isAdult) {
where = where.and(liveTag.tag.notIn("음담패설", "EDPS"))
}
return queryFactory
.select(
QGetLiveTagResponse(
liveTag.id,
liveTag.tag,
liveTag.image.prepend("/").prepend(cloudFrontHost)
)
)
.from(liveTag)
.where(where)
.orderBy(liveTag.orders.asc())
.fetch()
}
}

View File

@ -0,0 +1,52 @@
package kr.co.vividnext.sodalive.live.tag
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
@Service
class LiveTagService(
private val repository: LiveTagRepository,
private val objectMapper: ObjectMapper,
private val s3Uploader: S3Uploader,
@Value("\${cloud.aws.s3.bucket}")
private val coverImageBucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun enrollmentLiveTag(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateLiveTagRequest::class.java)
tagExistCheck(request)
val tag = repository.save(LiveTag(request.tag))
val metadata = ObjectMetadata()
metadata.contentLength = image.size
val tagImageFileName = generateFileName(prefix = "${tag.id}-")
val tagImagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = coverImageBucket,
filePath = "live_cover/${tag.id}/$tagImageFileName",
metadata = metadata
)
tag.image = tagImagePath
}
fun getTags(member: Member): List<GetLiveTagResponse> {
return repository.getTags(role = member.role, isAdult = member.auth != null, cloudFrontHost = cloudFrontHost)
}
fun tagExistCheck(request: CreateLiveTagRequest) {
repository.findByTag(request.tag)?.let { throw SodaException("이미 등록된 태그 입니다.") }
}
}