diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionController.kt new file mode 100644 index 0000000..ebba99c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionController.kt @@ -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) + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt new file mode 100644 index 0000000..dbdbe16 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt @@ -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, AdminAuditionQueryRepository + +interface AdminAuditionQueryRepository { + fun getAuditionList(offset: Long, limit: Long): List + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt new file mode 100644 index 0000000..b0ad818 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/CreateAuditionRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/CreateAuditionRequest.kt new file mode 100644 index 0000000..ccf7a10 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/CreateAuditionRequest.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt new file mode 100644 index 0000000..7a692a1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt @@ -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 +) + +data class GetAuditionRoleListData @QueryProjection constructor( + val id: Long, + val name: String, + val imageUrl: String, + val information: String, + val auditionScriptUrl: String, + val status: AuditionStatus +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt new file mode 100644 index 0000000..8d63ef2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt @@ -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 +) + +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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt new file mode 100644 index 0000000..a90be2d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleController.kt new file mode 100644 index 0000000..947f132 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleController.kt @@ -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() + ) + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleRepository.kt new file mode 100644 index 0000000..3503306 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleRepository.kt @@ -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, AdminAuditionRoleQueryRepository + +interface AdminAuditionRoleQueryRepository { + fun getAuditionRoleListByAuditionId(auditionId: Long): List + fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse + fun getAuditionApplicantList(auditionRoleId: Long, offset: Long, limit: Long): List + 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 { + 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 { + 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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleService.kt new file mode 100644 index 0000000..5e5b24a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleService.kt @@ -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) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/CreateAuditionRoleRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/CreateAuditionRoleRequest.kt new file mode 100644 index 0000000..1332e8f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/CreateAuditionRoleRequest.kt @@ -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글자 입니다") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleApplicantResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleApplicantResponse.kt new file mode 100644 index 0000000..8a10251 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleApplicantResponse.kt @@ -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 +) + +data class GetAuditionRoleApplicantItem @QueryProjection constructor( + val applicantId: Long, + val nickname: String, + val profileImageUrl: String, + val voiceUrl: String, + val voteCount: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleDetailResponse.kt new file mode 100644 index 0000000..2c3232c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleDetailResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/UpdateAuditionRoleRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/UpdateAuditionRoleRequest.kt new file mode 100644 index 0000000..6454bf0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/UpdateAuditionRoleRequest.kt @@ -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("잘못된 요청입니다.") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt new file mode 100644 index 0000000..ce29dd6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt @@ -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 +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt new file mode 100644 index 0000000..fcba813 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt @@ -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 +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt new file mode 100644 index 0000000..ce4593c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt new file mode 100644 index 0000000..1c63e6f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt @@ -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, AuditionQueryRepository + +interface AuditionQueryRepository { + fun getInProgressAuditionCount(isAdult: Boolean): Int + fun getCompletedAuditionCount(isAdult: Boolean): Int + fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt new file mode 100644 index 0000000..b482787 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt @@ -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 +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt new file mode 100644 index 0000000..2f6286c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionStatus.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionStatus.kt new file mode 100644 index 0000000..58a5c03 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionStatus.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.audition + +enum class AuditionStatus { + NOT_STARTED, + IN_PROGRESS, + COMPLETED +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionVote.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionVote.kt new file mode 100644 index 0000000..8e37808 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionVote.kt @@ -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 +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionDetailResponse.kt new file mode 100644 index 0000000..4a99d8c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionDetailResponse.kt @@ -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 +) + +data class GetAuditionRoleListData @QueryProjection constructor( + val roleId: Long, + val name: String, + val imageUrl: String, + val isComplete: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionListResponse.kt new file mode 100644 index 0000000..67452cd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionListResponse.kt @@ -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 +) + +data class GetAuditionListItem @QueryProjection constructor( + val id: Long, + val title: String, + val imageUrl: String, + val isOff: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/ApplyAuditionRoleRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/ApplyAuditionRoleRequest.kt new file mode 100644 index 0000000..c5b7d4e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/ApplyAuditionRoleRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.audition.applicant + +data class ApplyAuditionRoleRequest( + val roleId: Long, + val phoneNumber: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantController.kt new file mode 100644 index 0000000..9caeb2d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantController.kt @@ -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 + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt new file mode 100644 index 0000000..92af3f3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt @@ -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, 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 + + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt new file mode 100644 index 0000000..92c410e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt @@ -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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantSortType.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantSortType.kt new file mode 100644 index 0000000..649dc1e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantSortType.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.audition.applicant + +enum class AuditionApplicantSortType { + NEWEST, LIKES +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/GetAuditionApplicantListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/GetAuditionApplicantListResponse.kt new file mode 100644 index 0000000..188e17b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/GetAuditionApplicantListResponse.kt @@ -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 +) + +data class GetAuditionRoleApplicantItem @QueryProjection constructor( + val applicantId: Long, + val memberId: Long, + val nickname: String, + val profileImageUrl: String, + val voiceUrl: String, + val voteCount: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleController.kt new file mode 100644 index 0000000..5f7a48b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleController.kt @@ -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!! + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt new file mode 100644 index 0000000..b914f58 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt @@ -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, AuditionRoleQueryRepository + +interface AuditionRoleQueryRepository { + fun getAuditionRoleListByAuditionId(auditionId: Long): List + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleService.kt new file mode 100644 index 0000000..c1e05d3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleService.kt @@ -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) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/GetAuditionRoleDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/GetAuditionRoleDetailResponse.kt new file mode 100644 index 0000000..c49287c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/GetAuditionRoleDetailResponse.kt @@ -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 + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteController.kt new file mode 100644 index 0000000..434783b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteController.kt @@ -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 + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteRepository.kt new file mode 100644 index 0000000..584ed3d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteRepository.kt @@ -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, 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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt new file mode 100644 index 0000000..34261ea --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt @@ -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) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt new file mode 100644 index 0000000..2fb1957 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.audition.vote + +data class VoteAuditionApplicantRequest( + val applicantId: Long, + val timezone: String, + val container: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt index 87025af..33466a4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt @@ -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!! diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt index c50f823..5f60109 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -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 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt index 6677b8f..0b26698 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt @@ -8,5 +8,6 @@ enum class CanUsage { ORDER_CONTENT, SPIN_ROULETTE, PAID_COMMUNITY_POST, - ALARM_SLOT + ALARM_SLOT, + AUDITION_VOTE } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt index 31ac687..5a879f4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt @@ -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 = mutableListOf() } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt index 8b66af3..feb7daa 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt @@ -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? diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt index 5286adf..7c3c8ce 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -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() @@ -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,37 +192,57 @@ class ExplorerService( } // 추천 크리에이터 - val similarCreatorList = queryRepository.getSimilarCreatorList(creatorId) + val similarCreatorList = if (isCreator) { + queryRepository.getSimilarCreatorList(creatorId) + } else { + listOf() + } // 라이브 - val liveRoomList = queryRepository.getLiveRoomList( - creatorId, - userMember = member, - timezone = timezone, - limit = 3 - ) + val liveRoomList = if (isCreator) { + queryRepository.getLiveRoomList( + creatorId, + userMember = member, + timezone = timezone, + limit = 3 + ) + } else { + listOf() + } // 오디오 콘텐츠 - val contentList = audioContentService.getAudioContentList( - creatorId = creatorId, - sortType = SortType.NEWEST, - member = member, - offset = 0, - limit = 3 - ).items + 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( - creatorId = creatorId, - memberId = member.id!!, - timezone = timezone, - offset = 0, - limit = 3, - isAdult = member.auth != null - ) + val communityPostList = if (isCreator) { + communityService.getCommunityPostList( + creatorId = creatorId, + memberId = member.id!!, + timezone = timezone, + offset = 0, + 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 liveCount = queryRepository.getLiveCount(creatorId) ?: 0 - val liveTime = queryRepository.getLiveTime(creatorId) - val liveContributorCount = queryRepository.getLiveContributorCount(creatorId) ?: 0 - val contentCount = queryRepository.getContentCount(creatorId) ?: 0 + 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 - .getSeriesList(creatorId = creatorId, member = member) - .items + 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 ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt index bfdd016..5e80a77 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt @@ -15,7 +15,8 @@ data class GetCreatorProfileResponse( val cheers: GetCheersResponse, val activitySummary: GetCreatorActivitySummary, val seriesList: List, - val isBlock: Boolean + val isBlock: Boolean, + val isCreatorRole: Boolean ) data class GetCreatorActivitySummary(