From 99fdf473ae84df6cf348067582fae0d091dcc248 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 25 Dec 2024 02:39:21 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=98=A4?= =?UTF-8?q?=EB=94=94=EC=85=98=20=EC=BA=90=EB=A6=AD=ED=84=B0=20-=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95,=20=EC=83=81=EC=84=B8,?= =?UTF-8?q?=20=EC=A7=80=EC=9B=90=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../role/AdminAuditionRoleController.kt | 45 +++++++++++ .../role/AdminAuditionRoleRepository.kt | 73 +++++++++++++++++ .../audition/role/AdminAuditionRoleService.kt | 81 +++++++++++++++++++ .../role/CreateAuditionRoleRequest.kt | 23 ++++++ .../role/GetAuditionRoleApplicantResponse.kt | 16 ++++ .../role/GetAuditionRoleDetailResponse.kt | 9 +++ .../role/UpdateAuditionRoleRequest.kt | 16 ++++ .../sodalive/audition/AuditionRole.kt | 9 ++- 8 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/CreateAuditionRoleRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleApplicantResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleDetailResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/UpdateAuditionRoleRequest.kt 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..f13a248 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleController.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.admin.audition.role + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.data.domain.Pageable +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 +@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..c4fdbac --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleRepository.kt @@ -0,0 +1,73 @@ +package kr.co.vividnext.sodalive.admin.audition.role + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.audition.AuditionRole +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 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 getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse { + return queryFactory + .select( + QGetAuditionRoleDetailResponse( + auditionRole.name, + auditionRole.imagePath.prepend("/").prepend(cloudfrontHost), + 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..371c10c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleService.kt @@ -0,0 +1,81 @@ +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, 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() && request.name.length > 2) { + auditionRole.name = request.name + } + + if (!request.auditionScriptUrl.isNullOrBlank()) { + auditionRole.auditionScriptUrl = request.auditionScriptUrl + } + + 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..71f13d7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/CreateAuditionRoleRequest.kt @@ -0,0 +1,23 @@ +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 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을 입력하세요") + } + } +} 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..41e014c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleDetailResponse.kt @@ -0,0 +1,9 @@ +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 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..82e6ab8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/UpdateAuditionRoleRequest.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.admin.audition.role + +import kr.co.vividnext.sodalive.common.SodaException + +data class UpdateAuditionRoleRequest( + val id: Long, + val name: String? = null, + val auditionScriptUrl: String? = null, + val isActive: Boolean? = null +) { + init { + if (id < 0) { + throw SodaException("잘못된 요청입니다.") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt index 4f7250e..bd511c0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt @@ -8,12 +8,13 @@ import javax.persistence.ManyToOne @Entity data class AuditionRole( - val name: String, - val imagePath: String, + var name: String, // 오디션 대본 URL - val auditionScriptUrl: String, - val isActive: Boolean + var auditionScriptUrl: String? = null ) : BaseEntity() { + var isActive: Boolean = true + var imagePath: String? = null + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "audition_id", nullable = false) var audition: Audition? = null