From e29e71b8bdc7a249e17526f488ae6c7931aaf8e5 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 24 Dec 2024 03:26:20 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20-=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=9E=91=EC=84=B1=20-=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20-=20=EC=98=A4=EB=94=94=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1,=20=EC=88=98=EC=A0=95,=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/audition/AdminAuditionController.kt | 37 ++++++++ .../admin/audition/AdminAuditionRepository.kt | 70 +++++++++++++++ .../admin/audition/AdminAuditionService.kt | 90 +++++++++++++++++++ .../admin/audition/CreateAuditionRequest.kt | 45 ++++++++++ .../admin/audition/GetAuditionListResponse.kt | 19 ++++ .../admin/audition/UpdateAuditionRequest.kt | 11 +++ .../vividnext/sodalive/audition/Audition.kt | 18 ++++ .../sodalive/audition/AuditionApplicant.kt | 23 +++++ .../sodalive/audition/AuditionRole.kt | 20 +++++ .../sodalive/audition/AuditionVote.kt | 18 ++++ 10 files changed, 351 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/CreateAuditionRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionVote.kt 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..ab1e9ef --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionController.kt @@ -0,0 +1,37 @@ +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.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() + ) + ) +} 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..f548802 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt @@ -0,0 +1,70 @@ +package kr.co.vividnext.sodalive.admin.audition + +import com.querydsl.core.types.dsl.DateTimePath +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.core.types.dsl.StringTemplate +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 +import java.time.LocalDateTime + +@Repository +interface AdminAuditionRepository : JpaRepository, AdminAuditionQueryRepository + +interface AdminAuditionQueryRepository { + fun getAuditionList(offset: Long, limit: Long): List + fun getAuditionListCount(): Int +} + +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, + getFormattedDate(audition.endDate), + audition.imagePath.prepend("/").prepend(coverImageHost), + audition.isAdult, + audition.isActive, + audition.information, + audition.originalWorkUrl + ) + ) + .from(audition) + .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 + } + + private fun getFormattedDate(dateTimePath: DateTimePath): StringTemplate { + return Expressions.stringTemplate( + "DATE_FORMAT({0}, {1})", + Expressions.dateTimeTemplate( + LocalDateTime::class.java, + "CONVERT_TZ({0},{1},{2})", + dateTimePath, + "UTC", + "Asia/Seoul" + ), + "%Y-%m-%d %H:%i:%s" + ) + } +} 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..bfec948 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt @@ -0,0 +1,90 @@ +package kr.co.vividnext.sodalive.admin.audition + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.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 +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class AdminAuditionService( + private val s3Uploader: S3Uploader, + private val objectMapper: ObjectMapper, + private val repository: AdminAuditionRepository, + + @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.endDateString != null) { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val endDate = LocalDateTime.parse(request.endDateString, dateTimeFormatter) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + audition.endDate = endDate + } + + 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) + } +} 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..647fa34 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/CreateAuditionRequest.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.admin.audition + +import kr.co.vividnext.sodalive.audition.Audition +import kr.co.vividnext.sodalive.common.SodaException +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +data class CreateAuditionRequest( + val title: String, + val information: String, + val isAdult: Boolean = false, + val endDateString: String? = null, + val originalWorkUrl: String? = null +) { + init { + if (title.isBlank()) { + throw SodaException("오디션 제목을 입력하세요") + } + + if (information.isBlank() || information.length < 10) { + throw SodaException("오디션 정보는 최소 10글자 입니다") + } + } + + fun toAudition(): Audition { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val endDate = if (endDateString != null) { + LocalDateTime.parse(endDateString, dateTimeFormatter) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + } else { + null + } + + return Audition( + title = title, + information = information, + isAdult = isAdult, + endDate = endDate, + originalWorkUrl = originalWorkUrl + ) + } +} 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..10f2db4 --- /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 + +data class GetAuditionListResponse( + val totalCount: Int, + val items: List +) + +data class GetAuditionListItem @QueryProjection constructor( + val id: Long, + val title: String, + val endDate: String, + val imageUrl: String, + val isAdult: Boolean, + val isActive: Boolean, + val information: String, + 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..5b1f16f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.admin.audition + +data class UpdateAuditionRequest( + val id: Long, + val title: String? = null, + val information: String? = null, + val isAdult: Boolean? = null, + val endDateString: String? = null, + val originalWorkUrl: String? = null, + val isActive: Boolean? = null +) 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..5970923 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.audition + +import kr.co.vividnext.sodalive.common.BaseEntity +import java.time.LocalDateTime +import javax.persistence.Entity + +@Entity +data class Audition( + var title: String, + var information: String, + var isAdult: Boolean = false, + var endDate: LocalDateTime? = null, + // 원작 URL + var originalWorkUrl: String? = null +) : 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..e485407 --- /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 voicePath: String, + val phoneNumber: String, + val 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/AuditionRole.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt new file mode 100644 index 0000000..4f7250e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.audition + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class AuditionRole( + val name: String, + val imagePath: String, + // 오디션 대본 URL + val auditionScriptUrl: String, + val isActive: Boolean +) : BaseEntity() { + @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/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 +}