From 036107d103a6b2a4aa61f69953e3ad3f6a870418 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 31 Jul 2023 02:04:32 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20-=20=EB=B0=A9=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0,=20=ED=83=9C=EA=B7=B8=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D,=20=ED=83=9C=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/can/CanRepository.kt | 18 ++ .../sodalive/extensions/StringExtensions.kt | 9 + .../live/recommend/LiveRecommendRepository.kt | 2 - .../live/reservation/LiveReservation.kt | 27 +++ .../live/room/CreateLiveRoomResponse.kt | 6 + .../live/room/CreateSudaRoomRequest.kt | 15 ++ .../vividnext/sodalive/live/room/LiveRoom.kt | 13 +- .../sodalive/live/room/LiveRoomController.kt | 28 +++ .../sodalive/live/room/LiveRoomRepository.kt | 27 ++- .../sodalive/live/room/LiveRoomService.kt | 202 +++++++++++++++++- .../sodalive/live/room/LiveRoomTag.kt | 19 ++ .../live/room/detail/GetRoomDetailResponse.kt | 66 ++++++ .../sodalive/live/tag/CreateLiveTagRequest.kt | 3 + .../sodalive/live/tag/GetLiveTagResponse.kt | 9 + .../co/vividnext/sodalive/live/tag/LiveTag.kt | 17 ++ .../sodalive/live/tag/LiveTagController.kt | 35 +++ .../sodalive/live/tag/LiveTagRepository.kt | 40 ++++ .../sodalive/live/tag/LiveTagService.kt | 52 +++++ 18 files changed, 579 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt index 2220329..6c2f964 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt @@ -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.PaymentStatus 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.UseCan +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Member 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.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -23,6 +26,7 @@ interface CanQueryRepository { fun findAllByStatus(status: CanStatus): List fun getCanUseStatus(member: Member, pageable: Pageable): List fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List + fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan? } @Repository @@ -93,4 +97,18 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue .orderBy(charge.id.desc()) .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() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt new file mode 100644 index 0000000..ff1bb02 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt @@ -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) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt index 7ebacf8..7d1ebba 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -4,7 +4,6 @@ import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory 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.member.MemberRole import kr.co.vividnext.sodalive.member.QMember.member @@ -63,7 +62,6 @@ class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) { .where( where .and(liveRoom.isActive.isTrue) - .and(liveRoom.type.ne(LiveRoomType.SECRET)) .and(liveRoom.channelName.isNotNull) .and(liveRoom.channelName.isNotEmpty) ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt new file mode 100644 index 0000000..59cf6f2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt @@ -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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt new file mode 100644 index 0000000..24fb1be --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.live.room + +data class CreateLiveRoomResponse( + val id: Long?, + val channelName: String? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt new file mode 100644 index 0000000..2df98d8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt @@ -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, + val numberOfPeople: Int, + val beginDateTimeString: String? = null, + val price: Int = 0, + val timezone: String, + val type: LiveRoomType = LiveRoomType.OPEN, + val password: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt index 5137e5c..85035a3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt @@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive.live.room import kr.co.vividnext.sodalive.can.use.UseCan import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.reservation.LiveReservation import kr.co.vividnext.sodalive.member.Member import java.time.LocalDateTime +import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Entity import javax.persistence.EnumType @@ -33,9 +35,15 @@ data class LiveRoom( @JoinColumn(name = "member_id", nullable = false) var member: Member? = null + @OneToMany(mappedBy = "room", cascade = [CascadeType.ALL]) + var tags: MutableList = mutableListOf() + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) var useCan: MutableList = mutableListOf() + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) + var reservations: MutableList = mutableListOf() + var channelName: String? = null var isActive: Boolean = true } @@ -45,10 +53,7 @@ enum class LiveRoomType { OPEN, // 비공개 - PRIVATE, - - // 비밀방 - SECRET + PRIVATE } enum class LiveRoomStatus { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index ec927e4..c15d880 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -6,9 +6,13 @@ import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal 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.RequestParam +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/live/room") @@ -26,4 +30,28 @@ class LiveRoomController(private val service: LiveRoomService) { 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("로그인 정보를 확인해주세요.") + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index 82e227d..169ae95 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -5,6 +5,8 @@ import com.querydsl.core.types.Predicate import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.Expressions 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.member.Member import kr.co.vividnext.sodalive.member.QMember @@ -29,6 +31,9 @@ interface LiveRoomQueryRepository { timezone: String, isAdult: Boolean ): List + + fun getLiveRoom(id: Long): LiveRoom? + fun getReservationList(roomId: Long): List } class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { @@ -79,7 +84,6 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L where = where.and(liveRoom.isActive.isTrue) .and(liveRoom.member.isNotNull) - .and(liveRoom.type.ne(LiveRoomType.SECRET)) return queryFactory .selectFrom(liveRoom) @@ -98,6 +102,27 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L .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 { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveRoom.id.eq(roomId) + .and(liveReservation.isActive.isTrue) + ) + .fetch() + } + private fun orderByFieldAccountId( memberId: Long, status: LiveRoomStatus, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 24a41b8..3115000 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -1,11 +1,24 @@ 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.tag.LiveTagRepository import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile +import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -14,8 +27,15 @@ class LiveRoomService( private val repository: LiveRoomRepository, 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}") - private val coverImageHost: String + private val cloudFrontHost: String ) { fun getRoomList( dateString: String?, @@ -61,7 +81,7 @@ class LiveRoomService( coverImageUrl = if (it.coverImage!!.startsWith("https://")) { it.coverImage!! } else { - "$coverImageHost/${it.coverImage!!}" + "$cloudFrontHost/${it.coverImage!!}" }, isReservation = false, isPrivateRoom = it.type == LiveRoomType.PRIVATE @@ -69,4 +89,182 @@ class LiveRoomService( } .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 + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt new file mode 100644 index 0000000..c822931 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt @@ -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() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt new file mode 100644 index 0000000..8a8a0d8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt @@ -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, + val channelName: String?, + val beginDateTime: String, + var numberOfParticipants: Int, + val numberOfParticipantsTotal: Int +) { + var manager: GetRoomDetailManager? = null + var participatingUsers: List = 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" + } + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt new file mode 100644 index 0000000..80a1b59 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.live.tag + +data class CreateLiveTagRequest(val tag: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt new file mode 100644 index 0000000..e478871 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt new file mode 100644 index 0000000..99b0735 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt @@ -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() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt new file mode 100644 index 0000000..d72d1d6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt new file mode 100644 index 0000000..009768f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt @@ -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, LiveTagQueryRepository { + fun findByTag(it: String): LiveTag? +} + +interface LiveTagQueryRepository { + fun getTags(role: MemberRole, isAdult: Boolean, cloudFrontHost: String): List +} + +@Repository +class LiveTagQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveTagQueryRepository { + override fun getTags(role: MemberRole, isAdult: Boolean, cloudFrontHost: String): List { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt new file mode 100644 index 0000000..e168af4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt @@ -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 { + return repository.getTags(role = member.role, isAdult = member.auth != null, cloudFrontHost = cloudFrontHost) + } + + fun tagExistCheck(request: CreateLiveTagRequest) { + repository.findByTag(request.tag)?.let { throw SodaException("이미 등록된 태그 입니다.") } + } +}