Merge pull request '오디션' (#249) from test into main

Reviewed-on: #249
This commit is contained in:
klaus 2025-01-07 17:24:40 +00:00
commit 12a35db6cd
45 changed files with 1624 additions and 41 deletions

View File

@ -0,0 +1,43 @@
package kr.co.vividnext.sodalive.admin.audition
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
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.PutMapping
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
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/audition")
class AdminAuditionController(private val service: AdminAuditionService) {
@PostMapping
fun createAudition(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.createAudition(image, requestString), "등록되었습니다.")
@PutMapping
fun updateAudition(
@RequestPart("image", required = false) image: MultipartFile? = null,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.updateAudition(image, requestString), "수정되었습니다.")
@GetMapping
fun getAuditionList(pageable: Pageable) = ApiResponse.ok(
service.getAuditionList(
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
@GetMapping("/{id}")
fun getAuditionDetail(@PathVariable id: Long) = ApiResponse.ok(
service.getAuditionDetail(auditionId = id)
)
}

View File

@ -0,0 +1,69 @@
package kr.co.vividnext.sodalive.admin.audition
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.audition.Audition
import kr.co.vividnext.sodalive.audition.QAudition.audition
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AdminAuditionRepository : JpaRepository<Audition, Long>, AdminAuditionQueryRepository
interface AdminAuditionQueryRepository {
fun getAuditionList(offset: Long, limit: Long): List<GetAuditionListItem>
fun getAuditionListCount(): Int
fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData
}
class AdminAuditionQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val coverImageHost: String
) : AdminAuditionQueryRepository {
override fun getAuditionList(offset: Long, limit: Long): List<GetAuditionListItem> {
return queryFactory
.select(
QGetAuditionListItem(
audition.id,
audition.title,
audition.imagePath.prepend("/").prepend(coverImageHost),
audition.isAdult,
audition.information,
audition.status,
audition.originalWorkUrl.coalesce("")
)
)
.from(audition)
.where(audition.isActive.isTrue)
.offset(offset)
.limit(limit)
.orderBy(audition.isActive.desc(), audition.id.desc())
.fetch()
}
override fun getAuditionListCount(): Int {
return queryFactory
.select(audition.id)
.from(audition)
.fetch()
.size
}
override fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData {
return queryFactory
.select(
QGetAuditionDetailRawData(
audition.id,
audition.title,
audition.imagePath.prepend("/").prepend(coverImageHost),
audition.information,
audition.originalWorkUrl.coalesce("")
)
)
.from(audition)
.where(audition.id.eq(auditionId))
.fetchFirst()
}
}

View File

@ -0,0 +1,106 @@
package kr.co.vividnext.sodalive.admin.audition
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.audition.role.AdminAuditionRoleRepository
import kr.co.vividnext.sodalive.audition.AuditionStatus
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AdminAuditionService(
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val repository: AdminAuditionRepository,
private val roleRepository: AdminAuditionRoleRepository,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
@Transactional
fun createAudition(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateAuditionRequest::class.java)
val audition = repository.save(request.toAudition())
val fileName = generateFileName("audition")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/production/${audition.id}/$fileName"
)
audition.imagePath = imagePath
}
@Transactional
fun updateAudition(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateAuditionRequest::class.java)
val audition = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
if (request.title != null) {
audition.title = request.title
}
if (request.information != null) {
audition.information = request.information
}
if (request.isAdult != null) {
audition.isAdult = request.isAdult
}
if (request.status != null) {
if (
(audition.status == AuditionStatus.COMPLETED || audition.status == AuditionStatus.IN_PROGRESS) &&
request.status == AuditionStatus.NOT_STARTED
) {
throw SodaException("모집전 상태로 변경할 수 없습니다.")
}
audition.status = request.status
}
if (request.originalWorkUrl != null) {
audition.originalWorkUrl = request.originalWorkUrl
}
if (image != null) {
val fileName = generateFileName("audition")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/production/${audition.id}/$fileName"
)
audition.imagePath = imagePath
}
if (request.isActive != null) {
audition.isActive = request.isActive
}
}
fun getAuditionList(offset: Long, limit: Long): GetAuditionListResponse {
val totalCount = repository.getAuditionListCount()
val items = repository.getAuditionList(offset = offset, limit = limit)
return GetAuditionListResponse(totalCount, items)
}
fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse {
val auditionDetail = repository.getAuditionDetail(auditionId = auditionId)
val roleList = roleRepository.getAuditionRoleListByAuditionId(auditionId = auditionId)
return GetAuditionDetailResponse(
id = auditionDetail.id,
title = auditionDetail.title,
imageUrl = auditionDetail.imageUrl,
information = auditionDetail.information,
originalWorkUrl = auditionDetail.originalWorkUrl,
roleList = roleList
)
}
}

View File

@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.audition
import kr.co.vividnext.sodalive.audition.Audition
import kr.co.vividnext.sodalive.common.SodaException
data class CreateAuditionRequest(
val title: String,
val information: String,
val isAdult: Boolean = false,
val originalWorkUrl: String? = null
) {
init {
if (title.isBlank()) {
throw SodaException("오디션 제목을 입력하세요")
}
if (information.isBlank() || information.length < 10) {
throw SodaException("오디션 정보는 최소 10글자 입니다")
}
}
fun toAudition(): Audition {
return Audition(
title = title,
information = information,
isAdult = isAdult,
originalWorkUrl = originalWorkUrl
)
}
}

View File

@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.audition
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.audition.AuditionStatus
data class GetAuditionDetailRawData @QueryProjection constructor(
val id: Long,
val title: String,
val imageUrl: String,
val information: String,
val originalWorkUrl: String
)
data class GetAuditionDetailResponse(
val id: Long,
val title: String,
val imageUrl: String,
val information: String,
val originalWorkUrl: String,
val roleList: List<GetAuditionRoleListData>
)
data class GetAuditionRoleListData @QueryProjection constructor(
val id: Long,
val name: String,
val imageUrl: String,
val information: String,
val auditionScriptUrl: String,
val status: AuditionStatus
)

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.audition
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.audition.AuditionStatus
data class GetAuditionListResponse(
val totalCount: Int,
val items: List<GetAuditionListItem>
)
data class GetAuditionListItem @QueryProjection constructor(
val id: Long,
val title: String,
val imageUrl: String,
val isAdult: Boolean,
val information: String,
val status: AuditionStatus,
val originalWorkUrl: String
)

View File

@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.admin.audition
import kr.co.vividnext.sodalive.audition.AuditionStatus
data class UpdateAuditionRequest(
val id: Long,
val title: String? = null,
val information: String? = null,
val isAdult: Boolean? = null,
val status: AuditionStatus? = null,
val originalWorkUrl: String? = null,
val isActive: Boolean? = null
)

View File

@ -0,0 +1,47 @@
package kr.co.vividnext.sodalive.admin.audition.role
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
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.PutMapping
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
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/audition/role")
class AdminAuditionRoleController(private val service: AdminAuditionRoleService) {
@PostMapping
fun createAuditionRole(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.createAuditionRole(image, requestString), "등록되었습니다.")
@PutMapping
fun updateAuditionRole(
@RequestPart("image", required = false) image: MultipartFile? = null,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.updateAuditionRole(image, requestString), "수정되었습니다.")
@GetMapping("/{id}")
fun getAuditionRoleDetail(@PathVariable id: Long) = ApiResponse.ok(
service.getAuditionRoleDetail(auditionRoleId = id)
)
@GetMapping("/{id}/applicant")
fun getAuditionApplicantList(
@PathVariable id: Long,
pageable: Pageable
) = ApiResponse.ok(
service.getAuditionApplicantList(
auditionRoleId = id,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}

View File

@ -0,0 +1,99 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.audition.GetAuditionRoleListData
import kr.co.vividnext.sodalive.admin.audition.QGetAuditionRoleListData
import kr.co.vividnext.sodalive.audition.AuditionRole
import kr.co.vividnext.sodalive.audition.QAudition.audition
import kr.co.vividnext.sodalive.audition.QAuditionApplicant.auditionApplicant
import kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
import kr.co.vividnext.sodalive.audition.QAuditionVote.auditionVote
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface AdminAuditionRoleRepository : JpaRepository<AuditionRole, Long>, AdminAuditionRoleQueryRepository
interface AdminAuditionRoleQueryRepository {
fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData>
fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse
fun getAuditionApplicantList(auditionRoleId: Long, offset: Long, limit: Long): List<GetAuditionRoleApplicantItem>
fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int
}
class AdminAuditionRoleQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudfrontHost: String
) : AdminAuditionRoleQueryRepository {
override fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData> {
return queryFactory
.select(
QGetAuditionRoleListData(
auditionRole.id,
auditionRole.name,
auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
auditionRole.information,
auditionRole.auditionScriptUrl,
auditionRole.status
)
)
.from(auditionRole)
.innerJoin(auditionRole.audition, audition)
.where(
auditionRole.audition.id.eq(auditionId),
auditionRole.isActive.isTrue
)
.fetch()
}
override fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse {
return queryFactory
.select(
QGetAuditionRoleDetailResponse(
auditionRole.name,
auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
auditionRole.information,
auditionRole.auditionScriptUrl
)
)
.from(auditionRole)
.where(auditionRole.id.eq(auditionRoleId))
.fetchFirst()
}
override fun getAuditionApplicantList(
auditionRoleId: Long,
offset: Long,
limit: Long
): List<GetAuditionRoleApplicantItem> {
return queryFactory
.select(
QGetAuditionRoleApplicantItem(
auditionApplicant.id,
member.nickname,
member.profileImage.prepend("/").prepend(cloudfrontHost),
auditionApplicant.voicePath.prepend("/").prepend(cloudfrontHost),
auditionVote.id.count()
)
)
.from(auditionApplicant)
.innerJoin(auditionApplicant.member, member)
.innerJoin(auditionApplicant.role, auditionRole)
.leftJoin(auditionVote).on(auditionApplicant.id.eq(auditionVote.applicant.id))
.where(auditionRole.id.eq(auditionRoleId))
.groupBy(auditionApplicant.id)
.fetch()
}
override fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int {
return queryFactory
.select(auditionApplicant.id)
.from(auditionApplicant)
.innerJoin(auditionApplicant.role, auditionRole)
.where(auditionRole.id.eq(auditionRoleId))
.fetch()
.size
}
}

View File

@ -0,0 +1,95 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.audition.AdminAuditionRepository
import kr.co.vividnext.sodalive.audition.AuditionRole
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AdminAuditionRoleService(
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val repository: AdminAuditionRoleRepository,
private val auditionRepository: AdminAuditionRepository,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
@Transactional
fun createAuditionRole(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateAuditionRoleRequest::class.java)
val auditionRole = AuditionRole(
name = request.name,
information = request.information,
auditionScriptUrl = request.auditionScriptUrl
)
val audition = auditionRepository.findByIdOrNull(id = request.auditionId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
auditionRole.audition = audition
repository.save(auditionRole)
val fileName = generateFileName("audition_role")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/role/${auditionRole.id}/$fileName"
)
auditionRole.imagePath = imagePath
}
@Transactional
fun updateAuditionRole(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateAuditionRoleRequest::class.java)
val auditionRole = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
if (!request.name.isNullOrBlank()) {
if (request.name.length < 2) throw SodaException("배역 이름은 최소 2글자 입니다")
auditionRole.name = request.name
}
if (!request.information.isNullOrBlank()) {
if (request.information.length < 10) throw SodaException("오디션 배역 정보는 최소 10글자 입니다")
auditionRole.information = request.information
}
if (!request.auditionScriptUrl.isNullOrBlank()) {
auditionRole.auditionScriptUrl = request.auditionScriptUrl
}
if (request.status != null) {
auditionRole.status = request.status
}
if (request.isActive != null) {
auditionRole.isActive = request.isActive
}
if (image != null) {
val fileName = generateFileName("audition_role")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/role/${auditionRole.id}/$fileName"
)
auditionRole.imagePath = imagePath
}
}
fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse {
return repository.getAuditionRoleDetail(auditionRoleId = auditionRoleId)
}
fun getAuditionApplicantList(auditionRoleId: Long, offset: Long, limit: Long): GetAuditionRoleApplicantResponse {
val totalCount = repository.getAuditionApplicantTotalCount(auditionRoleId = auditionRoleId)
val items = repository.getAuditionApplicantList(auditionRoleId = auditionRoleId, offset = offset, limit = limit)
return GetAuditionRoleApplicantResponse(totalCount, items)
}
}

View File

@ -0,0 +1,28 @@
package kr.co.vividnext.sodalive.admin.audition.role
import kr.co.vividnext.sodalive.common.SodaException
data class CreateAuditionRoleRequest(
val auditionId: Long,
val name: String,
val information: String,
val auditionScriptUrl: String
) {
init {
if (auditionId < 0) {
throw SodaException("캐릭터가 등록될 오디션을 선택하세요")
}
if (name.isBlank() || name.length < 2) {
throw SodaException("캐릭터명을 입력하세요")
}
if (auditionScriptUrl.isBlank() || auditionScriptUrl.length < 10) {
throw SodaException("오디션 대본 URL을 입력하세요")
}
if (information.isBlank() || information.length < 10) {
throw SodaException("오디션 캐릭터 정보는 최소 10글자 입니다")
}
}
}

View File

@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionRoleApplicantResponse(
val totalCount: Int,
val items: List<GetAuditionRoleApplicantItem>
)
data class GetAuditionRoleApplicantItem @QueryProjection constructor(
val applicantId: Long,
val nickname: String,
val profileImageUrl: String,
val voiceUrl: String,
val voteCount: Long
)

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionRoleDetailResponse @QueryProjection constructor(
val name: String,
val imageUrl: String,
val information: String,
val auditionScriptUrl: String
)

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.audition.role
import kr.co.vividnext.sodalive.audition.AuditionStatus
import kr.co.vividnext.sodalive.common.SodaException
data class UpdateAuditionRoleRequest(
val id: Long,
val name: String? = null,
val information: String? = null,
val auditionScriptUrl: String? = null,
val status: AuditionStatus? = null,
val isActive: Boolean? = null
) {
init {
if (id < 0) {
throw SodaException("잘못된 요청입니다.")
}
}
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.audition
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
@Entity
data class Audition(
var title: String,
@Column(columnDefinition = "TEXT")
var information: String,
var isAdult: Boolean = false,
// 원작 URL
var originalWorkUrl: String? = null,
@Enumerated(value = EnumType.STRING)
var status: AuditionStatus = AuditionStatus.NOT_STARTED
) : BaseEntity() {
var isActive: Boolean = true
var imagePath: String? = null
}

View File

@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audition
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.Member
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
@Entity
data class AuditionApplicant(
val phoneNumber: String,
var voicePath: String? = null,
var isActive: Boolean = true
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", nullable = false)
var role: AuditionRole? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member? = null
}

View File

@ -0,0 +1,41 @@
package kr.co.vividnext.sodalive.audition
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
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.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/audition")
class AuditionController(private val service: AuditionService) {
@GetMapping
fun getAuditionList(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(
service.getAuditionList(
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
isAdult = member.auth != null
)
)
}
@GetMapping("/{id}")
fun getAuditionDetail(
@PathVariable id: Long,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getAuditionDetail(auditionId = id))
}
}

View File

@ -0,0 +1,97 @@
package kr.co.vividnext.sodalive.audition
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.audition.QAudition.audition
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface AuditionRepository : JpaRepository<Audition, Long>, AuditionQueryRepository
interface AuditionQueryRepository {
fun getInProgressAuditionCount(isAdult: Boolean): Int
fun getCompletedAuditionCount(isAdult: Boolean): Int
fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem>
fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData
}
class AuditionQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : AuditionQueryRepository {
override fun getInProgressAuditionCount(isAdult: Boolean): Int {
var where = audition.isActive.isTrue
.and(audition.status.eq(AuditionStatus.IN_PROGRESS))
if (!isAdult) {
where = where.and(audition.isAdult.isFalse)
}
return queryFactory
.select(audition.id)
.from(audition)
.where(where)
.fetch()
.size
}
override fun getCompletedAuditionCount(isAdult: Boolean): Int {
var where = audition.isActive.isTrue
.and(audition.status.eq(AuditionStatus.COMPLETED))
if (!isAdult) {
where = where.and(audition.isAdult.isFalse)
}
return queryFactory
.select(audition.id)
.from(audition)
.where(where)
.fetch()
.size
}
override fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem> {
var where = audition.isActive.isTrue
.and(
audition.status.eq(AuditionStatus.COMPLETED)
.or(audition.status.eq(AuditionStatus.IN_PROGRESS))
)
if (!isAdult) {
where = where.and(audition.isAdult.isFalse)
}
return queryFactory
.select(
QGetAuditionListItem(
audition.id,
audition.title,
audition.imagePath.prepend("/").prepend(cloudFrontHost),
audition.status.eq(AuditionStatus.COMPLETED)
)
)
.from(audition)
.where(where)
.offset(offset)
.limit(limit)
.orderBy(audition.status.desc())
.fetch()
}
override fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData {
return queryFactory
.select(
QGetAuditionDetailRawData(
audition.id,
audition.title,
audition.imagePath.prepend("/").prepend(cloudFrontHost),
audition.information
)
)
.from(audition)
.where(audition.id.eq(auditionId))
.fetchFirst()
}
}

View File

@ -0,0 +1,28 @@
package kr.co.vividnext.sodalive.audition
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
@Entity
data class AuditionRole(
var name: String,
@Column(columnDefinition = "TEXT")
var information: String,
// 오디션 대본 URL
var auditionScriptUrl: String? = null,
@Enumerated(value = EnumType.STRING)
var status: AuditionStatus = AuditionStatus.IN_PROGRESS
) : BaseEntity() {
var isActive: Boolean = true
var imagePath: String? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "audition_id", nullable = false)
var audition: Audition? = null
}

View File

@ -0,0 +1,31 @@
package kr.co.vividnext.sodalive.audition
import kr.co.vividnext.sodalive.audition.role.AuditionRoleRepository
import org.springframework.stereotype.Service
@Service
class AuditionService(
private val repository: AuditionRepository,
private val roleRepository: AuditionRoleRepository
) {
fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): GetAuditionListResponse {
val inProgressCount = repository.getInProgressAuditionCount(isAdult = isAdult)
val completedCount = repository.getCompletedAuditionCount(isAdult = isAdult)
val items = repository.getAuditionList(offset = offset, limit = limit, isAdult = isAdult)
return GetAuditionListResponse(inProgressCount, completedCount, items)
}
fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse {
val auditionDetail = repository.getAuditionDetail(auditionId = auditionId)
val roleList = roleRepository.getAuditionRoleListByAuditionId(auditionId = auditionId)
return GetAuditionDetailResponse(
auditionId = auditionId,
title = auditionDetail.title,
imageUrl = auditionDetail.imageUrl,
information = auditionDetail.information,
roleList = roleList
)
}
}

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.audition
enum class AuditionStatus {
NOT_STARTED,
IN_PROGRESS,
COMPLETED
}

View File

@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.audition
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.Member
import javax.persistence.Entity
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
@Entity
class AuditionVote : BaseEntity() {
@ManyToOne
@JoinColumn(name = "member_id", nullable = false)
var member: Member? = null
@ManyToOne
@JoinColumn(name = "applicant_id", nullable = false)
var applicant: AuditionApplicant? = null
}

View File

@ -0,0 +1,25 @@
package kr.co.vividnext.sodalive.audition
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionDetailRawData @QueryProjection constructor(
val auditionId: Long,
val title: String,
val imageUrl: String,
val information: String
)
data class GetAuditionDetailResponse(
val auditionId: Long,
val title: String,
val imageUrl: String,
val information: String,
val roleList: List<GetAuditionRoleListData>
)
data class GetAuditionRoleListData @QueryProjection constructor(
val roleId: Long,
val name: String,
val imageUrl: String,
val isComplete: Boolean
)

View File

@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.audition
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionListResponse(
val inProgressCount: Int,
val completedCount: Int,
val items: List<GetAuditionListItem>
)
data class GetAuditionListItem @QueryProjection constructor(
val id: Long,
val title: String,
val imageUrl: String,
val isOff: Boolean
)

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.audition.applicant
data class ApplyAuditionRoleRequest(
val roleId: Long,
val phoneNumber: String
)

View File

@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.audition.applicant
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
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.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("/audition/applicant")
class AuditionApplicantController(private val service: AuditionApplicantService) {
@GetMapping
fun getAuditionApplicantList(
@RequestParam auditionRoleId: Long,
@RequestParam sortType: AuditionApplicantSortType,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(
service.getAuditionApplicantList(
auditionRoleId = auditionRoleId,
sortType = sortType,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}
@PostMapping
fun applyAuditionRole(
@RequestPart("contentFile")
contentFile: MultipartFile?,
@RequestPart("request") requestString: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(
service.applyAuditionRole(
contentFile = contentFile,
requestString = requestString,
member = member
)
)
}
}

View File

@ -0,0 +1,109 @@
package kr.co.vividnext.sodalive.audition.applicant
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.audition.QAuditionApplicant.auditionApplicant
import kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
import kr.co.vividnext.sodalive.audition.QAuditionVote.auditionVote
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface AuditionApplicantRepository : JpaRepository<AuditionApplicant, Long>, AuditionApplicantQueryRepository
interface AuditionApplicantQueryRepository {
fun isAlreadyApplicant(auditionRoleId: Long, memberId: Long): Boolean
fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int
fun getAuditionApplicantList(
auditionRoleId: Long,
sortType: AuditionApplicantSortType,
offset: Long,
limit: Long
): List<GetAuditionRoleApplicantItem>
fun findActiveApplicantByMemberIdAndRoleId(memberId: Long, roleId: Long): AuditionApplicant?
}
class AuditionApplicantQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : AuditionApplicantQueryRepository {
override fun isAlreadyApplicant(auditionRoleId: Long, memberId: Long): Boolean {
return queryFactory
.select(auditionApplicant.id)
.from(auditionApplicant)
.innerJoin(auditionApplicant.role, auditionRole)
.innerJoin(auditionApplicant.member, member)
.where(
auditionRole.id.eq(auditionRoleId),
member.id.eq(memberId),
auditionApplicant.isActive.isTrue
)
.fetch()
.size > 0
}
override fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int {
return queryFactory
.select(auditionApplicant.id)
.from(auditionApplicant)
.innerJoin(auditionApplicant.role, auditionRole)
.where(
auditionRole.id.eq(auditionRoleId),
auditionApplicant.isActive.isTrue
)
.fetch()
.size
}
override fun getAuditionApplicantList(
auditionRoleId: Long,
sortType: AuditionApplicantSortType,
offset: Long,
limit: Long
): List<GetAuditionRoleApplicantItem> {
val orderBy = if (sortType == AuditionApplicantSortType.LIKES) {
auditionVote.id.count().desc()
} else {
auditionApplicant.id.desc()
}
return queryFactory
.select(
QGetAuditionRoleApplicantItem(
auditionApplicant.id,
member.id,
member.nickname,
member.profileImage.prepend("/").prepend(cloudFrontHost),
auditionApplicant.voicePath.prepend("/").prepend(cloudFrontHost),
auditionVote.id.count()
)
)
.from(auditionApplicant)
.innerJoin(auditionApplicant.member, member)
.innerJoin(auditionApplicant.role, auditionRole)
.leftJoin(auditionVote).on(auditionApplicant.id.eq(auditionVote.applicant.id))
.where(
auditionRole.id.eq(auditionRoleId),
auditionApplicant.isActive.isTrue
)
.groupBy(auditionApplicant.id)
.orderBy(orderBy)
.offset(offset)
.limit(limit)
.fetch()
}
override fun findActiveApplicantByMemberIdAndRoleId(memberId: Long, roleId: Long): AuditionApplicant? {
return queryFactory
.selectFrom(auditionApplicant)
.where(
auditionApplicant.isActive.isTrue
.and(auditionApplicant.member.id.eq(memberId))
.and(auditionApplicant.role.id.eq(roleId))
)
.fetchFirst()
}
}

View File

@ -0,0 +1,84 @@
package kr.co.vividnext.sodalive.audition.applicant
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.audition.role.AuditionRoleRepository
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.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AuditionApplicantService(
private val repository: AuditionApplicantRepository,
private val roleRepository: AuditionRoleRepository,
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
fun getAuditionApplicantList(
auditionRoleId: Long,
sortType: AuditionApplicantSortType,
offset: Long,
limit: Long
): GetAuditionApplicantListResponse {
val totalCount = repository.getAuditionApplicantTotalCount(auditionRoleId = auditionRoleId)
val items = repository.getAuditionApplicantList(
auditionRoleId = auditionRoleId,
sortType = sortType,
offset = offset,
limit = limit
)
return GetAuditionApplicantListResponse(
totalCount = totalCount,
items = items
)
}
@Transactional
fun applyAuditionRole(contentFile: MultipartFile?, requestString: String, member: Member) {
if (contentFile == null) throw SodaException("녹음 파일을 확인해 주세요.")
val request = objectMapper.readValue(requestString, ApplyAuditionRoleRequest::class.java)
val auditionRole = roleRepository.findByIdOrNull(id = request.roleId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
val existingApplicant = repository.findActiveApplicantByMemberIdAndRoleId(
memberId = member.id!!,
roleId = auditionRole.id!!
)
if (existingApplicant != null) {
existingApplicant.isActive = false
repository.save(existingApplicant)
}
val applicant = AuditionApplicant(phoneNumber = request.phoneNumber)
applicant.role = auditionRole
applicant.member = member
repository.save(applicant)
val contentFileName = generateFileName(prefix = "${applicant.id}-applicant")
val metadata = ObjectMetadata()
metadata.contentLength = contentFile.size
val contentPath = s3Uploader.upload(
inputStream = contentFile.inputStream,
bucket = bucket,
filePath = "audition/applicant/${applicant.id}/$contentFileName",
metadata = metadata
)
applicant.voicePath = contentPath
}
}

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.audition.applicant
enum class AuditionApplicantSortType {
NEWEST, LIKES
}

View File

@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.audition.applicant
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionApplicantListResponse(
val totalCount: Int,
val items: List<GetAuditionRoleApplicantItem>
)
data class GetAuditionRoleApplicantItem @QueryProjection constructor(
val applicantId: Long,
val memberId: Long,
val nickname: String,
val profileImageUrl: String,
val voiceUrl: String,
val voteCount: Long
)

View File

@ -0,0 +1,29 @@
package kr.co.vividnext.sodalive.audition.role
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.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/audition/role")
class AuditionRoleController(private val service: AuditionRoleService) {
@GetMapping("/{id}")
fun getAuditionRoleDetail(
@PathVariable id: Long,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(
service.getAuditionRoleDetail(
auditionRoleId = id,
memberId = member.id!!
)
)
}
}

View File

@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.audition.role
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.audition.AuditionRole
import kr.co.vividnext.sodalive.audition.AuditionStatus
import kr.co.vividnext.sodalive.audition.GetAuditionRoleListData
import kr.co.vividnext.sodalive.audition.QAudition.audition
import kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
import kr.co.vividnext.sodalive.audition.QGetAuditionRoleListData
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface AuditionRoleRepository : JpaRepository<AuditionRole, Long>, AuditionRoleQueryRepository
interface AuditionRoleQueryRepository {
fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData>
fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailData?
}
class AuditionRoleQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudfrontHost: String
) : AuditionRoleQueryRepository {
override fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData> {
return queryFactory
.select(
QGetAuditionRoleListData(
auditionRole.id,
auditionRole.name,
auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
auditionRole.status.eq(AuditionStatus.COMPLETED)
)
)
.from(auditionRole)
.innerJoin(auditionRole.audition, audition)
.where(
audition.id.eq(auditionId),
auditionRole.isActive.isTrue
)
.orderBy(auditionRole.status.desc(), auditionRole.id.desc())
.fetch()
}
override fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailData? {
return queryFactory
.select(
QGetAuditionRoleDetailData(
auditionRole.id,
auditionRole.name,
auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
auditionRole.information,
audition.originalWorkUrl,
auditionRole.auditionScriptUrl
)
)
.from(auditionRole)
.innerJoin(auditionRole.audition, audition)
.where(
auditionRole.id.eq(auditionRoleId),
auditionRole.isActive.isTrue
)
.fetchFirst()
}
}

View File

@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audition.role
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantRepository
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.stereotype.Service
@Service
class AuditionRoleService(
private val repository: AuditionRoleRepository,
private val applicantRepository: AuditionApplicantRepository
) {
fun getAuditionRoleDetail(auditionRoleId: Long, memberId: Long): GetAuditionRoleDetailResponse {
val roleDetailData = repository.getAuditionRoleDetail(auditionRoleId = auditionRoleId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
val isAlreadyApplicant = applicantRepository.isAlreadyApplicant(
auditionRoleId = auditionRoleId,
memberId = memberId
)
return roleDetailData.toDetailResponse(isAlreadyApplicant = isAlreadyApplicant)
}
}

View File

@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.audition.role
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionRoleDetailResponse(
val auditionRoleId: Long,
val name: String,
val imageUrl: String,
val information: String,
val originalWorkUrl: String,
val auditionScriptUrl: String,
val isAlreadyApplicant: Boolean = false
)
data class GetAuditionRoleDetailData @QueryProjection constructor(
val auditionRoleId: Long,
val name: String,
val imageUrl: String,
val information: String,
val originalWorkUrl: String,
val auditionScriptUrl: String
) {
fun toDetailResponse(isAlreadyApplicant: Boolean) = GetAuditionRoleDetailResponse(
auditionRoleId = auditionRoleId,
name = name,
imageUrl = imageUrl,
information = information,
originalWorkUrl = originalWorkUrl,
auditionScriptUrl = auditionScriptUrl,
isAlreadyApplicant = isAlreadyApplicant
)
}

View File

@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.audition.vote
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.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/audition/vote")
class AuditionVoteController(
private val service: AuditionVoteService
) {
@PostMapping
fun voteAuditionApplicant(
@RequestBody request: VoteAuditionApplicantRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(
service.voteAuditionApplicant(
applicantId = request.applicantId,
timezone = request.timezone,
container = request.container,
member = member
)
)
}
}

View File

@ -0,0 +1,37 @@
package kr.co.vividnext.sodalive.audition.vote
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.audition.AuditionVote
import kr.co.vividnext.sodalive.audition.QAuditionVote.auditionVote
import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalDateTime
interface AuditionVoteRepository : JpaRepository<AuditionVote, Long>, AuditionVoteQueryRepository
interface AuditionVoteQueryRepository {
fun countByMemberIdAndVoteDateRange(
memberId: Long,
startDate: LocalDateTime,
endDate: LocalDateTime
): Int
}
class AuditionVoteQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory
) : AuditionVoteQueryRepository {
override fun countByMemberIdAndVoteDateRange(
memberId: Long,
startDate: LocalDateTime,
endDate: LocalDateTime
): Int {
return queryFactory
.select(auditionVote.id)
.from(auditionVote)
.where(
auditionVote.member.id.eq(memberId)
.and(auditionVote.createdAt.between(startDate, endDate))
)
.fetch()
.size
}
}

View File

@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.audition.vote
import kr.co.vividnext.sodalive.audition.AuditionVote
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantRepository
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.ZonedDateTime
@Service
class AuditionVoteService(
private val repository: AuditionVoteRepository,
private val applicantRepository: AuditionApplicantRepository,
private val canPaymentService: CanPaymentService
) {
fun voteAuditionApplicant(applicantId: Long, timezone: String, container: String, member: Member) {
val applicant = applicantRepository.findByIdOrNull(applicantId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
val defaultZoneId = ZoneId.of("Asia/Seoul")
val clientZoneId = try {
ZoneId.of(timezone)
} catch (e: Exception) {
defaultZoneId
}
val nowInClientZone = ZonedDateTime.now(clientZoneId)
val startOfDayClient = nowInClientZone.toLocalDate().atStartOfDay(clientZoneId)
val endOfDayClient = startOfDayClient.plusDays(1).minusSeconds(1)
val startDate = startOfDayClient.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
val endDate = endOfDayClient.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
val voteCount = repository.countByMemberIdAndVoteDateRange(
memberId = member.id!!,
startDate = startDate,
endDate = endDate
)
if (voteCount > 10) {
throw SodaException("오늘 응원은 여기까지!\n하루 최대 10회까지 응원이 가능합니다.\n내일 다시 이용해주세요.")
}
if (voteCount > 0) {
canPaymentService.spendCan(
memberId = member.id!!,
needCan = 1,
canUsage = CanUsage.AUDITION_VOTE,
auditionApplicant = applicant,
container = container
)
}
val auditionVote = AuditionVote()
auditionVote.applicant = applicant
auditionVote.member = member
repository.save(auditionVote)
}
}

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.audition.vote
data class VoteAuditionApplicantRequest(
val applicantId: Long,
val timezone: String,
val container: String
)

View File

@ -71,6 +71,7 @@ class CanService(private val repository: CanRepository) {
CanUsage.ALARM_SLOT -> "알람 슬롯 구매"
CanUsage.ORDER_CONTENT -> "[콘텐츠 구매] ${it.audioContent!!.title}"
CanUsage.PAID_COMMUNITY_POST -> "[게시글 보기] ${it.communityPost?.member?.nickname ?: ""}"
CanUsage.AUDITION_VOTE -> "[오디션 투표] ${it.auditionApplicant?.role?.audition?.title ?: ""}"
}
val createdAt = it.createdAt!!

View File

@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.can.payment
import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
@ -41,6 +42,7 @@ class CanPaymentService(
order: Order? = null,
audioContent: AudioContent? = null,
communityPost: CreatorCommunity? = null,
auditionApplicant: AuditionApplicant? = null,
container: String
) {
val member = memberRepository.findByIdOrNull(id = memberId)
@ -100,6 +102,9 @@ class CanPaymentService(
useCan.member = member
} else if (canUsage == CanUsage.ALARM_SLOT) {
useCan.member = member
} else if (canUsage == CanUsage.AUDITION_VOTE && auditionApplicant != null) {
useCan.auditionApplicant = auditionApplicant
useCan.member = member
} else if (canUsage == CanUsage.HEART && liveRoom != null) {
recipientId = liveRoom.member!!.id!!
useCan.room = liveRoom

View File

@ -8,5 +8,6 @@ enum class CanUsage {
ORDER_CONTENT,
SPIN_ROULETTE,
PAID_COMMUNITY_POST,
ALARM_SLOT
ALARM_SLOT,
AUDITION_VOTE
}

View File

@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.can.use
import kr.co.vividnext.sodalive.audition.AuditionApplicant
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.order.Order
@ -53,6 +54,10 @@ data class UseCan(
@JoinColumn(name = "creator_community_id", nullable = true)
var communityPost: CreatorCommunity? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "audition_applicant_id", nullable = true)
var auditionApplicant: AuditionApplicant? = null
@OneToMany(mappedBy = "useCan", cascade = [CascadeType.ALL])
val useCanCalculates: MutableList<UseCanCalculate> = mutableListOf()
}

View File

@ -21,6 +21,15 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/explorer")
class ExplorerController(private val service: ExplorerService) {
@GetMapping("/creator-rank")
fun getCreatorRank(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getCreatorRank(memberId = member.id!!))
}
@GetMapping
fun getExplorer(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?

View File

@ -47,6 +47,34 @@ class ExplorerService(
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getCreatorRank(memberId: Long): GetExplorerSectionResponse {
val creatorRankings = queryRepository
.getCreatorRankings()
.filter { !memberService.isBlocked(blockedMemberId = memberId, memberId = it.id!!) }
.map { it.toExplorerSectionCreator(cloudFrontHost) }
val currentDateTime = LocalDateTime.now()
val lastMonday = currentDateTime
.minusWeeks(1)
.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
val lastSunday = lastMonday
.plusDays(6)
val startDateFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")
val endDateFormatter = DateTimeFormatter.ofPattern("MM월 dd일")
val formattedLastMonday = lastMonday.format(startDateFormatter)
val formattedLastSunday = lastSunday.format(endDateFormatter)
return GetExplorerSectionResponse(
title = "인기 크리에이터",
coloredTitle = "인기",
color = "FF5C49",
desc = "$formattedLastMonday ~ $formattedLastSunday",
creators = creatorRankings
)
}
fun getExplorer(member: Member, growthRankingCreatorsLimit: Long = 20): GetExplorerResponse {
val sections = mutableListOf<GetExplorerSectionResponse>()
@ -144,12 +172,16 @@ class ExplorerService(
val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
if (isBlocked) throw SodaException("${creatorAccount.nickname}님의 요청으로 채널 접근이 제한됩니다.")
val isCreator = creatorAccount.role == MemberRole.CREATOR
val notificationUserIds = queryRepository.getNotificationUserIds(creatorId)
val creatorFollowing = queryRepository.getCreatorFollowing(creatorId = creatorId, memberId = member.id!!)
val notificationRecipientCount = notificationUserIds.size
// 후원랭킹
val memberDonationRanking = if (creatorId == member.id!! || creatorAccount.isVisibleDonationRank) {
val memberDonationRanking = if (
isCreator && (creatorId == member.id!! || creatorAccount.isVisibleDonationRank)
) {
queryRepository.getMemberDonationRanking(
creatorId,
10,
@ -160,30 +192,47 @@ class ExplorerService(
}
// 추천 크리에이터
val similarCreatorList = queryRepository.getSimilarCreatorList(creatorId)
val similarCreatorList = if (isCreator) {
queryRepository.getSimilarCreatorList(creatorId)
} else {
listOf()
}
// 라이브
val liveRoomList = queryRepository.getLiveRoomList(
val liveRoomList = if (isCreator) {
queryRepository.getLiveRoomList(
creatorId,
userMember = member,
timezone = timezone,
limit = 3
)
} else {
listOf()
}
// 오디오 콘텐츠
val contentList = audioContentService.getAudioContentList(
val contentList = if (isCreator) {
audioContentService.getAudioContentList(
creatorId = creatorId,
sortType = SortType.NEWEST,
member = member,
offset = 0,
limit = 3
).items
} else {
listOf()
}
// 공지사항
val notice = queryRepository.getNoticeString(creatorId)
val notice = if (isCreator) {
queryRepository.getNoticeString(creatorId)
} else {
""
}
// 커뮤니티
val communityPostList = communityService.getCommunityPostList(
val communityPostList = if (isCreator) {
communityService.getCommunityPostList(
creatorId = creatorId,
memberId = member.id!!,
timezone = timezone,
@ -191,6 +240,9 @@ class ExplorerService(
limit = 3,
isAdult = member.auth != null
)
} else {
listOf()
}
// 응원
val cheers = queryRepository.getCheersList(creatorId, timezone = timezone, offset = 0, limit = 4)
@ -198,15 +250,29 @@ class ExplorerService(
// 차단한 크리에이터 인지 체크
val isBlock = memberService.isBlocked(blockedMemberId = creatorId, memberId = member.id!!)
val activitySummary = if (isCreator) {
// 활동요약 (라이브 횟수, 라이브 시간, 라이브 참여자, 콘텐츠 수)
val liveCount = queryRepository.getLiveCount(creatorId) ?: 0
val liveTime = queryRepository.getLiveTime(creatorId)
val liveContributorCount = queryRepository.getLiveContributorCount(creatorId) ?: 0
val contentCount = queryRepository.getContentCount(creatorId) ?: 0
GetCreatorActivitySummary(
liveCount = liveCount,
liveTime = liveTime,
liveContributorCount = liveContributorCount,
contentCount = contentCount
)
} else {
GetCreatorActivitySummary(0, 0, 0, 0)
}
val seriesList = seriesService
val seriesList = if (isCreator) {
seriesService
.getSeriesList(creatorId = creatorId, member = member)
.items
} else {
listOf()
}
return GetCreatorProfileResponse(
creator = CreatorResponse(
@ -235,14 +301,10 @@ class ExplorerService(
notice = notice,
communityPostList = communityPostList,
cheers = cheers,
activitySummary = GetCreatorActivitySummary(
liveCount = liveCount,
liveTime = liveTime,
liveContributorCount = liveContributorCount,
contentCount = contentCount
),
activitySummary = activitySummary,
seriesList = seriesList,
isBlock = isBlock
isBlock = isBlock,
isCreatorRole = isCreator
)
}

View File

@ -15,7 +15,8 @@ data class GetCreatorProfileResponse(
val cheers: GetCheersResponse,
val activitySummary: GetCreatorActivitySummary,
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
val isBlock: Boolean
val isBlock: Boolean,
val isCreatorRole: Boolean
)
data class GetCreatorActivitySummary(