From e29e71b8bdc7a249e17526f488ae6c7931aaf8e5 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Tue, 24 Dec 2024 03:26:20 +0900
Subject: [PATCH 01/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20-=20?=
 =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=9E=91=EC=84=B1=20-=20?=
 =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20=EC=98=A4=EB=94=94=EC=85=98?=
 =?UTF-8?q?=20=EC=83=9D=EC=84=B1,=20=EC=88=98=EC=A0=95,=20=EB=A6=AC?=
 =?UTF-8?q?=EC=8A=A4=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<Audition, Long>, AdminAuditionQueryRepository
+
+interface AdminAuditionQueryRepository {
+    fun getAuditionList(offset: Long, limit: Long): List<GetAuditionListItem>
+    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<GetAuditionListItem> {
+        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<LocalDateTime>): 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<GetAuditionListItem>
+)
+
+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
+}

From bb3263dd68f5d01a3a4efda24ae463392165e531 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Tue, 24 Dec 2024 16:28:11 +0900
Subject: [PATCH 02/40] =?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=83=81=EC=84=B8=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/audition/AdminAuditionController.kt |  6 +++++
 .../admin/audition/AdminAuditionRepository.kt | 27 +++++++++++++++++++
 .../admin/audition/AdminAuditionService.kt    |  4 +++
 .../audition/GetAuditionDetailResponse.kt     | 18 +++++++++++++
 4 files changed, 55 insertions(+)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.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
index ab1e9ef..ebba99c 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionController.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionController.kt
@@ -4,6 +4,7 @@ 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
@@ -34,4 +35,9 @@ class AdminAuditionController(private val service: AdminAuditionService) {
             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
index f548802..6e0ad41 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -1,11 +1,13 @@
 package kr.co.vividnext.sodalive.admin.audition
 
+import com.querydsl.core.group.GroupBy.list
 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 kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
 import org.springframework.beans.factory.annotation.Value
 import org.springframework.data.jpa.repository.JpaRepository
 import org.springframework.stereotype.Repository
@@ -17,6 +19,7 @@ interface AdminAuditionRepository : JpaRepository<Audition, Long>, AdminAudition
 interface AdminAuditionQueryRepository {
     fun getAuditionList(offset: Long, limit: Long): List<GetAuditionListItem>
     fun getAuditionListCount(): Int
+    fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse
 }
 
 class AdminAuditionQueryRepositoryImpl(
@@ -54,6 +57,30 @@ class AdminAuditionQueryRepositoryImpl(
             .size
     }
 
+    override fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse {
+        return queryFactory
+            .select(
+                QGetAuditionDetailResponse(
+                    audition.id,
+                    audition.title,
+                    audition.imagePath.prepend("/").prepend(coverImageHost),
+                    audition.information,
+                    audition.originalWorkUrl,
+                    list(
+                        QGetAuditionDetailRole(
+                            auditionRole.id,
+                            auditionRole.name,
+                            auditionRole.imagePath.prepend("/").prepend(coverImageHost)
+                        )
+                    )
+                )
+            )
+            .from(audition)
+            .leftJoin(auditionRole).on(auditionRole.audition.id.eq(audition.id))
+            .where(audition.id.eq(auditionId))
+            .fetchFirst()
+    }
+
     private fun getFormattedDate(dateTimePath: DateTimePath<LocalDateTime>): StringTemplate {
         return Expressions.stringTemplate(
             "DATE_FORMAT({0}, {1})",
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
index bfec948..5127a00 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
@@ -87,4 +87,8 @@ class AdminAuditionService(
         val items = repository.getAuditionList(offset = offset, limit = limit)
         return GetAuditionListResponse(totalCount, items)
     }
+
+    fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse {
+        return repository.getAuditionDetail(auditionId = auditionId)
+    }
 }
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..803aca6
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
@@ -0,0 +1,18 @@
+package kr.co.vividnext.sodalive.admin.audition
+
+import com.querydsl.core.annotations.QueryProjection
+
+data class GetAuditionDetailResponse @QueryProjection constructor(
+    val id: Long,
+    val title: String,
+    val imageUrl: String,
+    val information: String,
+    val originalWorkUrl: String,
+    val roleList: List<GetAuditionDetailRole> = listOf()
+)
+
+data class GetAuditionDetailRole @QueryProjection constructor(
+    val id: Long,
+    val name: String,
+    val imageUrl: String
+)

From 99fdf473ae84df6cf348067582fae0d091dcc248 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Wed, 25 Dec 2024 02:39:21 +0900
Subject: [PATCH 03/40] =?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<AuditionRole, Long>, AdminAuditionRoleQueryRepository
+
+interface AdminAuditionRoleQueryRepository {
+    fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse
+    fun getAuditionApplicantList(auditionRoleId: Long, offset: Long, limit: Long): List<GetAuditionRoleApplicantItem>
+    fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int
+}
+
+class AdminAuditionRoleQueryRepositoryImpl(
+    private val queryFactory: JPAQueryFactory,
+
+    @Value("\${cloud.aws.cloud-front.host}")
+    private val cloudfrontHost: String
+) : AdminAuditionRoleQueryRepository {
+    override fun 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<GetAuditionRoleApplicantItem> {
+        return queryFactory
+            .select(
+                QGetAuditionRoleApplicantItem(
+                    auditionApplicant.id,
+                    member.nickname,
+                    member.profileImage.prepend("/").prepend(cloudfrontHost),
+                    auditionApplicant.voicePath.prepend("/").prepend(cloudfrontHost),
+                    auditionVote.id.count()
+                )
+            )
+            .from(auditionApplicant)
+            .innerJoin(auditionApplicant.member, member)
+            .innerJoin(auditionApplicant.role, auditionRole)
+            .leftJoin(auditionVote).on(auditionApplicant.id.eq(auditionVote.applicant.id))
+            .where(auditionRole.id.eq(auditionRoleId))
+            .groupBy(auditionApplicant.id)
+            .fetch()
+    }
+
+    override fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int {
+        return queryFactory
+            .select(auditionApplicant.id)
+            .from(auditionApplicant)
+            .innerJoin(auditionApplicant.role, auditionRole)
+            .where(auditionRole.id.eq(auditionRoleId))
+            .fetch()
+            .size
+    }
+}
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<GetAuditionRoleApplicantItem>
+)
+
+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

From d940b3092f4c39159ca73841a71416066802b826 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Wed, 25 Dec 2024 02:46:55 +0900
Subject: [PATCH 04/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=97=94?=
 =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20-=20information=20column=20type=EC=9D=84?=
 =?UTF-8?q?=20TEXT=EB=A1=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt
index 5970923..9de0835 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt
@@ -2,11 +2,13 @@ package kr.co.vividnext.sodalive.audition
 
 import kr.co.vividnext.sodalive.common.BaseEntity
 import java.time.LocalDateTime
+import javax.persistence.Column
 import javax.persistence.Entity
 
 @Entity
 data class Audition(
     var title: String,
+    @Column(columnDefinition = "TEXT")
     var information: String,
     var isAdult: Boolean = false,
     var endDate: LocalDateTime? = null,

From 86450533cf791a4ff69a15adb7027e489b15ecea Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Thu, 26 Dec 2024 22:43:15 +0900
Subject: [PATCH 05/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=A6=AC?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API=20-=20endDate=EC=99=80=20=EC=9B=90?=
 =?UTF-8?q?=EC=9E=91=EB=A7=81=ED=81=AC=EA=B0=80=20null=EC=9D=B8=20?=
 =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=B9=88=20=EC=B9=B8=EC=9C=BC=EB=A1=9C=20?=
 =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/AdminAuditionRepository.kt    | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

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
index 6e0ad41..29b3a95 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -1,6 +1,7 @@
 package kr.co.vividnext.sodalive.admin.audition
 
 import com.querydsl.core.group.GroupBy.list
+import com.querydsl.core.types.dsl.CaseBuilder
 import com.querydsl.core.types.dsl.DateTimePath
 import com.querydsl.core.types.dsl.Expressions
 import com.querydsl.core.types.dsl.StringTemplate
@@ -34,12 +35,15 @@ class AdminAuditionQueryRepositoryImpl(
                 QGetAuditionListItem(
                     audition.id,
                     audition.title,
-                    getFormattedDate(audition.endDate),
+                    CaseBuilder()
+                        .`when`(audition.endDate.isNotNull)
+                        .then(getFormattedDate(audition.endDate))
+                        .otherwise(""),
                     audition.imagePath.prepend("/").prepend(coverImageHost),
                     audition.isAdult,
                     audition.isActive,
                     audition.information,
-                    audition.originalWorkUrl
+                    audition.originalWorkUrl.coalesce("")
                 )
             )
             .from(audition)

From 22c302efa0afd5e0dfe27b132820e4f7c378927a Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 27 Dec 2024 01:56:45 +0900
Subject: [PATCH 06/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=97=94?=
 =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20-=20status(=EB=AA=A8=EC=A7=91=EC=83=81?=
 =?UTF-8?q?=ED=83=9C)=20=EC=B6=94=EA=B0=80=20-=20=EB=A6=AC=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=20api=20:=20=EC=9D=91=EB=8B=B5=EA=B0=92=EC=97=90=20st?=
 =?UTF-8?q?atus=20=EC=B6=94=EA=B0=80,=20=ED=99=9C=EC=84=B1=ED=99=94=20?=
 =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A7=8C=20=EC=A1=B0=ED=9A=8C=20-?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95=20api=20:=20status=20=EC=88=98=EC=A0=95?=
 =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/audition/AdminAuditionRepository.kt        |  3 ++-
 .../sodalive/admin/audition/AdminAuditionService.kt  | 12 ++++++++++++
 .../admin/audition/GetAuditionListResponse.kt        |  3 ++-
 .../sodalive/admin/audition/UpdateAuditionRequest.kt |  3 +++
 .../kr/co/vividnext/sodalive/audition/Audition.kt    |  6 +++++-
 .../co/vividnext/sodalive/audition/AuditionStatus.kt |  7 +++++++
 6 files changed, 31 insertions(+), 3 deletions(-)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionStatus.kt

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
index 29b3a95..dc2e8e1 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -41,12 +41,13 @@ class AdminAuditionQueryRepositoryImpl(
                         .otherwise(""),
                     audition.imagePath.prepend("/").prepend(coverImageHost),
                     audition.isAdult,
-                    audition.isActive,
                     audition.information,
+                    audition.status,
                     audition.originalWorkUrl.coalesce("")
                 )
             )
             .from(audition)
+            .where(audition.isActive.isTrue)
             .offset(offset)
             .limit(limit)
             .orderBy(audition.isActive.desc(), audition.id.desc())
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
index 5127a00..6358a23 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
@@ -1,6 +1,7 @@
 package kr.co.vividnext.sodalive.admin.audition
 
 import com.fasterxml.jackson.databind.ObjectMapper
+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
@@ -54,6 +55,17 @@ class AdminAuditionService(
             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.endDateString != null) {
             val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
             val endDate = LocalDateTime.parse(request.endDateString, dateTimeFormatter)
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
index 10f2db4..4858790 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt
@@ -1,6 +1,7 @@
 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,
@@ -13,7 +14,7 @@ data class GetAuditionListItem @QueryProjection constructor(
     val endDate: String,
     val imageUrl: String,
     val isAdult: Boolean,
-    val isActive: 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
index 5b1f16f..540e446 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt
@@ -1,11 +1,14 @@
 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 endDateString: String? = null,
+    val status: AuditionStatus? = 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
index 9de0835..24e2727 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt
@@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.common.BaseEntity
 import java.time.LocalDateTime
 import javax.persistence.Column
 import javax.persistence.Entity
+import javax.persistence.EnumType
+import javax.persistence.Enumerated
 
 @Entity
 data class Audition(
@@ -13,7 +15,9 @@ data class Audition(
     var isAdult: Boolean = false,
     var endDate: LocalDateTime? = null,
     // 원작 URL
-    var originalWorkUrl: String? = null
+    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/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
+}

From 8b10e0e7703655dff68f3c8594642a13b891af6e Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 27 Dec 2024 02:00:15 +0900
Subject: [PATCH 07/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD(=EC=BA=90=EB=A6=AD=ED=84=B0)=20=EC=97=94=ED=8B=B0?=
 =?UTF-8?q?=ED=8B=B0=20-=20status(=EB=AA=A8=EC=A7=91=EC=83=81=ED=83=9C)=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/AdminAuditionRepository.kt        | 3 ++-
 .../sodalive/admin/audition/GetAuditionDetailResponse.kt      | 4 +++-
 .../kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt  | 3 ++-
 3 files changed, 7 insertions(+), 3 deletions(-)

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
index dc2e8e1..c2b7827 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -75,7 +75,8 @@ class AdminAuditionQueryRepositoryImpl(
                         QGetAuditionDetailRole(
                             auditionRole.id,
                             auditionRole.name,
-                            auditionRole.imagePath.prepend("/").prepend(coverImageHost)
+                            auditionRole.imagePath.prepend("/").prepend(coverImageHost),
+                            auditionRole.status
                         )
                     )
                 )
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
index 803aca6..2cdc85a 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
@@ -1,6 +1,7 @@
 package kr.co.vividnext.sodalive.admin.audition
 
 import com.querydsl.core.annotations.QueryProjection
+import kr.co.vividnext.sodalive.audition.AuditionStatus
 
 data class GetAuditionDetailResponse @QueryProjection constructor(
     val id: Long,
@@ -14,5 +15,6 @@ data class GetAuditionDetailResponse @QueryProjection constructor(
 data class GetAuditionDetailRole @QueryProjection constructor(
     val id: Long,
     val name: String,
-    val imageUrl: String
+    val imageUrl: String,
+    val status: AuditionStatus
 )
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 bd511c0..fb43371 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt
@@ -10,7 +10,8 @@ import javax.persistence.ManyToOne
 data class AuditionRole(
     var name: String,
     // 오디션 대본 URL
-    var auditionScriptUrl: String? = null
+    var auditionScriptUrl: String? = null,
+    var status: AuditionStatus = AuditionStatus.IN_PROGRESS
 ) : BaseEntity() {
     var isActive: Boolean = true
     var imagePath: String? = null

From a3e717f2f7032cbb05adda0bfaca820254c4d2d8 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 27 Dec 2024 02:07:35 +0900
Subject: [PATCH 08/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD(=EC=BA=90=EB=A6=AD=ED=84=B0)=20=EC=97=94=ED=8B=B0?=
 =?UTF-8?q?=ED=8B=B0=20-=20status(=EB=AA=A8=EC=A7=91=EC=83=81=ED=83=9C)=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt   | 3 +++
 1 file changed, 3 insertions(+)

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 fb43371..40e8089 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.audition
 
 import kr.co.vividnext.sodalive.common.BaseEntity
 import javax.persistence.Entity
+import javax.persistence.EnumType
+import javax.persistence.Enumerated
 import javax.persistence.FetchType
 import javax.persistence.JoinColumn
 import javax.persistence.ManyToOne
@@ -11,6 +13,7 @@ data class AuditionRole(
     var name: String,
     // 오디션 대본 URL
     var auditionScriptUrl: String? = null,
+    @Enumerated(value = EnumType.STRING)
     var status: AuditionStatus = AuditionStatus.IN_PROGRESS
 ) : BaseEntity() {
     var isActive: Boolean = true

From a35b602f1a52c0ba02e1ca3ec2786d4b5cbc618a Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 27 Dec 2024 21:53:37 +0900
Subject: [PATCH 09/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=83=81?=
 =?UTF-8?q?=EC=84=B8=20-=20roleList=EC=9D=98=20=EC=A1=B0=ED=9A=8C=EA=B0=92?=
 =?UTF-8?q?=EC=9D=B4=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20emptyList?=
 =?UTF-8?q?=EB=A1=9C=20=EC=84=A0=EC=96=B8=EB=90=98=EB=8F=84=EB=A1=9D=20?=
 =?UTF-8?q?=EC=B2=98=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/AdminAuditionRepository.kt         | 3 ++-
 .../sodalive/admin/audition/GetAuditionDetailResponse.kt       | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

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
index c2b7827..5c9a753 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -70,7 +70,7 @@ class AdminAuditionQueryRepositoryImpl(
                     audition.title,
                     audition.imagePath.prepend("/").prepend(coverImageHost),
                     audition.information,
-                    audition.originalWorkUrl,
+                    audition.originalWorkUrl.coalesce(""),
                     list(
                         QGetAuditionDetailRole(
                             auditionRole.id,
@@ -84,6 +84,7 @@ class AdminAuditionQueryRepositoryImpl(
             .from(audition)
             .leftJoin(auditionRole).on(auditionRole.audition.id.eq(audition.id))
             .where(audition.id.eq(auditionId))
+            .groupBy(audition.id)
             .fetchFirst()
     }
 
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
index 2cdc85a..9ac5a2b 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
@@ -9,7 +9,7 @@ data class GetAuditionDetailResponse @QueryProjection constructor(
     val imageUrl: String,
     val information: String,
     val originalWorkUrl: String,
-    val roleList: List<GetAuditionDetailRole> = listOf()
+    val roleList: List<GetAuditionDetailRole> = emptyList()
 )
 
 data class GetAuditionDetailRole @QueryProjection constructor(

From 4f0a882b9e6deaf3b1c82148953ee374eacdee70 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 27 Dec 2024 22:05:32 +0900
Subject: [PATCH 10/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=83=81?=
 =?UTF-8?q?=EC=84=B8=20-=20groupBy=EC=97=90=20=EB=B9=84=EC=A7=91=EA=B3=84?=
 =?UTF-8?q?=EC=97=B4=20=EB=AA=A8=EB=91=90=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/AdminAuditionRepository.kt    | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

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
index 5c9a753..7bf3b17 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -84,7 +84,13 @@ class AdminAuditionQueryRepositoryImpl(
             .from(audition)
             .leftJoin(auditionRole).on(auditionRole.audition.id.eq(audition.id))
             .where(audition.id.eq(auditionId))
-            .groupBy(audition.id)
+            .groupBy(
+                audition.id,
+                audition.title,
+                audition.imagePath,
+                audition.information,
+                audition.originalWorkUrl
+            )
             .fetchFirst()
     }
 

From c9e90974bdcc58398f776318f957d18cf493b0e6 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 27 Dec 2024 23:19:21 +0900
Subject: [PATCH 11/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=83=81?=
 =?UTF-8?q?=EC=84=B8=20-=20=EC=98=A4=EB=94=94=EC=85=98=20=EB=8D=B0?=
 =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=99=80=20=EC=98=A4=EB=94=94=EC=85=98=20?=
 =?UTF-8?q?=EB=B0=B0=EC=97=AD=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=8D=B0?=
 =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=98=B8=EC=B6=9C=EC=9D=84=20=EB=B6=84?=
 =?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/audition/AdminAuditionRepository.kt | 26 +++----------------
 .../admin/audition/AdminAuditionService.kt    | 14 +++++++++-
 .../audition/GetAuditionDetailResponse.kt     | 14 +++++++---
 .../role/AdminAuditionRoleRepository.kt       | 20 ++++++++++++++
 4 files changed, 48 insertions(+), 26 deletions(-)

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
index 7bf3b17..a457e59 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -1,6 +1,5 @@
 package kr.co.vividnext.sodalive.admin.audition
 
-import com.querydsl.core.group.GroupBy.list
 import com.querydsl.core.types.dsl.CaseBuilder
 import com.querydsl.core.types.dsl.DateTimePath
 import com.querydsl.core.types.dsl.Expressions
@@ -8,7 +7,6 @@ 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 kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
 import org.springframework.beans.factory.annotation.Value
 import org.springframework.data.jpa.repository.JpaRepository
 import org.springframework.stereotype.Repository
@@ -20,7 +18,7 @@ interface AdminAuditionRepository : JpaRepository<Audition, Long>, AdminAudition
 interface AdminAuditionQueryRepository {
     fun getAuditionList(offset: Long, limit: Long): List<GetAuditionListItem>
     fun getAuditionListCount(): Int
-    fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse
+    fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData
 }
 
 class AdminAuditionQueryRepositoryImpl(
@@ -62,35 +60,19 @@ class AdminAuditionQueryRepositoryImpl(
             .size
     }
 
-    override fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse {
+    override fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData {
         return queryFactory
             .select(
-                QGetAuditionDetailResponse(
+                QGetAuditionDetailRawData(
                     audition.id,
                     audition.title,
                     audition.imagePath.prepend("/").prepend(coverImageHost),
                     audition.information,
-                    audition.originalWorkUrl.coalesce(""),
-                    list(
-                        QGetAuditionDetailRole(
-                            auditionRole.id,
-                            auditionRole.name,
-                            auditionRole.imagePath.prepend("/").prepend(coverImageHost),
-                            auditionRole.status
-                        )
-                    )
+                    audition.originalWorkUrl.coalesce("")
                 )
             )
             .from(audition)
-            .leftJoin(auditionRole).on(auditionRole.audition.id.eq(audition.id))
             .where(audition.id.eq(auditionId))
-            .groupBy(
-                audition.id,
-                audition.title,
-                audition.imagePath,
-                audition.information,
-                audition.originalWorkUrl
-            )
             .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
index 6358a23..774b5b8 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
@@ -1,6 +1,7 @@
 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
@@ -19,6 +20,7 @@ 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
@@ -101,6 +103,16 @@ class AdminAuditionService(
     }
 
     fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse {
-        return repository.getAuditionDetail(auditionId = auditionId)
+        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/GetAuditionDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
index 9ac5a2b..0071dd2 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
@@ -3,16 +3,24 @@ package kr.co.vividnext.sodalive.admin.audition
 import com.querydsl.core.annotations.QueryProjection
 import kr.co.vividnext.sodalive.audition.AuditionStatus
 
-data class GetAuditionDetailResponse @QueryProjection constructor(
+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<GetAuditionDetailRole> = emptyList()
+    val roleList: List<GetAuditionRoleListData>
 )
 
-data class GetAuditionDetailRole @QueryProjection constructor(
+data class GetAuditionRoleListData @QueryProjection constructor(
     val id: Long,
     val name: String,
     val imageUrl: String,
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
index c4fdbac..0c71c7f 100644
--- 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
@@ -1,7 +1,10 @@
 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
@@ -12,6 +15,7 @@ import org.springframework.data.jpa.repository.JpaRepository
 interface AdminAuditionRoleRepository : JpaRepository<AuditionRole, Long>, AdminAuditionRoleQueryRepository
 
 interface AdminAuditionRoleQueryRepository {
+    fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData>
     fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse
     fun getAuditionApplicantList(auditionRoleId: Long, offset: Long, limit: Long): List<GetAuditionRoleApplicantItem>
     fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int
@@ -23,6 +27,22 @@ class AdminAuditionRoleQueryRepositoryImpl(
     @Value("\${cloud.aws.cloud-front.host}")
     private val cloudfrontHost: String
 ) : AdminAuditionRoleQueryRepository {
+    override fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData> {
+        return queryFactory
+            .select(
+                QGetAuditionRoleListData(
+                    auditionRole.id,
+                    auditionRole.name,
+                    auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
+                    auditionRole.status
+                )
+            )
+            .from(auditionRole)
+            .innerJoin(auditionRole.audition, audition)
+            .where(auditionRole.audition.id.eq(auditionId))
+            .fetch()
+    }
+
     override fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse {
         return queryFactory
             .select(

From 1dba0a3d959e0927eb460ee47e651743d5dc811c Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 28 Dec 2024 01:11:26 +0900
Subject: [PATCH 12/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=83=81?=
 =?UTF-8?q?=EC=84=B8=20-=20=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=8D=B0=EC=9D=B4?=
 =?UTF-8?q?=ED=84=B0=EC=97=90=20=EB=8C=80=EB=B3=B8=20=EB=A7=81=ED=81=AC=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/GetAuditionDetailResponse.kt         | 1 +
 .../sodalive/admin/audition/role/AdminAuditionRoleRepository.kt  | 1 +
 2 files changed, 2 insertions(+)

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
index 0071dd2..8c0ddcd 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
@@ -24,5 +24,6 @@ data class GetAuditionRoleListData @QueryProjection constructor(
     val id: Long,
     val name: String,
     val imageUrl: String,
+    val auditionScriptUrl: String,
     val status: AuditionStatus
 )
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
index 0c71c7f..a830406 100644
--- 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
@@ -34,6 +34,7 @@ class AdminAuditionRoleQueryRepositoryImpl(
                     auditionRole.id,
                     auditionRole.name,
                     auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
+                    auditionRole.auditionScriptUrl,
                     auditionRole.status
                 )
             )

From 2e66b5fa457397af7e7fe697a8083975c2ab11dd Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 28 Dec 2024 01:40:16 +0900
Subject: [PATCH 13/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=83=81?=
 =?UTF-8?q?=EC=84=B8=20-=20=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20-=20=ED=99=9C?=
 =?UTF-8?q?=EC=84=B1=ED=99=94=EB=90=9C=20=EB=B0=B0=EC=97=AD=EB=A7=8C=20?=
 =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/audition/role/AdminAuditionRoleRepository.kt       | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

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
index a830406..92c3844 100644
--- 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
@@ -40,7 +40,10 @@ class AdminAuditionRoleQueryRepositoryImpl(
             )
             .from(auditionRole)
             .innerJoin(auditionRole.audition, audition)
-            .where(auditionRole.audition.id.eq(auditionId))
+            .where(
+                auditionRole.audition.id.eq(auditionId),
+                auditionRole.isActive.isTrue
+            )
             .fetch()
     }
 

From df3a00f8c0ed240bd6aa8f7875cf7db1058de496 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 28 Dec 2024 01:51:46 +0900
Subject: [PATCH 14/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD=20=EC=88=98=EC=A0=95=20-=20=EB=AA=A8=EC=A7=91=20?=
 =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=A0=20?=
 =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=B3=80=EC=88=98=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/role/UpdateAuditionRoleRequest.kt   | 2 ++
 1 file changed, 2 insertions(+)

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
index 82e6ab8..51ac6e2 100644
--- 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
@@ -1,11 +1,13 @@
 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 auditionScriptUrl: String? = null,
+    val status: AuditionStatus? = null,
     val isActive: Boolean? = null
 ) {
     init {

From 6e6b27bb65b4fe635ac5a2819cf7e62513ccdba5 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 28 Dec 2024 02:04:04 +0900
Subject: [PATCH 15/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD=20=EC=88=98=EC=A0=95=20-=20=EB=AA=A8=EC=A7=91=20?=
 =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=A0=20?=
 =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=B3=80=EC=88=98=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/role/AdminAuditionRoleService.kt  | 4 ++++
 1 file changed, 4 insertions(+)

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
index 371c10c..6c6ecc6 100644
--- 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
@@ -54,6 +54,10 @@ class AdminAuditionRoleService(
             auditionRole.auditionScriptUrl = request.auditionScriptUrl
         }
 
+        if (request.status != null) {
+            auditionRole.status = request.status
+        }
+
         if (request.isActive != null) {
             auditionRole.isActive = request.isActive
         }

From 8cfe9ade9a5ebcefab34a932f7f61b944e7115a4 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 28 Dec 2024 03:29:11 +0900
Subject: [PATCH 16/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD=20=EB=93=B1=EB=A1=9D/=EC=88=98=EC=A0=95=20-=20?=
 =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0=EC=97=AD=20=EC=A0=95?=
 =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/audition/role/AdminAuditionRoleRepository.kt |  1 +
 .../admin/audition/role/AdminAuditionRoleService.kt    | 10 +++++++++-
 .../admin/audition/role/CreateAuditionRoleRequest.kt   |  5 +++++
 .../audition/role/GetAuditionRoleDetailResponse.kt     |  1 +
 .../admin/audition/role/UpdateAuditionRoleRequest.kt   |  1 +
 .../kr/co/vividnext/sodalive/audition/AuditionRole.kt  |  3 +++
 6 files changed, 20 insertions(+), 1 deletion(-)

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
index 92c3844..7b6fb50 100644
--- 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
@@ -53,6 +53,7 @@ class AdminAuditionRoleQueryRepositoryImpl(
                 QGetAuditionRoleDetailResponse(
                     auditionRole.name,
                     auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
+                    auditionRole.information,
                     auditionRole.auditionScriptUrl
                 )
             )
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
index 6c6ecc6..37870ae 100644
--- 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
@@ -25,7 +25,11 @@ class AdminAuditionRoleService(
     @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 auditionRole = AuditionRole(
+            name = request.name,
+            information = request.information,
+            auditionScriptUrl = request.auditionScriptUrl
+        )
         val audition = auditionRepository.findByIdOrNull(id = request.auditionId)
             ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
         auditionRole.audition = audition
@@ -50,6 +54,10 @@ class AdminAuditionRoleService(
             auditionRole.name = request.name
         }
 
+        if (!request.information.isNullOrBlank() && request.information.length >= 10) {
+            auditionRole.information = request.information
+        }
+
         if (!request.auditionScriptUrl.isNullOrBlank()) {
             auditionRole.auditionScriptUrl = request.auditionScriptUrl
         }
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
index 71f13d7..1332e8f 100644
--- 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
@@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.common.SodaException
 data class CreateAuditionRoleRequest(
     val auditionId: Long,
     val name: String,
+    val information: String,
     val auditionScriptUrl: String
 ) {
     init {
@@ -19,5 +20,9 @@ data class CreateAuditionRoleRequest(
         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/GetAuditionRoleDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/GetAuditionRoleDetailResponse.kt
index 41e014c..2c3232c 100644
--- 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
@@ -5,5 +5,6 @@ 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
index 51ac6e2..6454bf0 100644
--- 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
@@ -6,6 +6,7 @@ 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
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 40e8089..b482787 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRole.kt
@@ -1,6 +1,7 @@
 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
@@ -11,6 +12,8 @@ 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)

From 44dfa45ca8b4f603fed514e78d7f80ffbb45630e Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 28 Dec 2024 03:40:04 +0900
Subject: [PATCH 17/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EB=B0=B0=EC=97=AD=20=EC=A0=95=EB=B3=B4=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/GetAuditionDetailResponse.kt         | 1 +
 .../sodalive/admin/audition/role/AdminAuditionRoleRepository.kt  | 1 +
 2 files changed, 2 insertions(+)

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
index 8c0ddcd..7a692a1 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionDetailResponse.kt
@@ -24,6 +24,7 @@ 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/role/AdminAuditionRoleRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/role/AdminAuditionRoleRepository.kt
index 7b6fb50..3503306 100644
--- 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
@@ -34,6 +34,7 @@ class AdminAuditionRoleQueryRepositoryImpl(
                     auditionRole.id,
                     auditionRole.name,
                     auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
+                    auditionRole.information,
                     auditionRole.auditionScriptUrl,
                     auditionRole.status
                 )

From bb41a81eb1cb4977087002b5ec99b9c8e012f23c Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 28 Dec 2024 03:50:43 +0900
Subject: [PATCH 18/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?=
 =?UTF-8?q?=EC=97=AD=20=EC=88=98=EC=A0=95=20-=20=EB=B0=B0=EC=97=AD=20?=
 =?UTF-8?q?=EC=9D=B4=EB=A6=84=EA=B3=BC=20=EB=B0=B0=EC=97=AD=20=EC=A0=95?=
 =?UTF-8?q?=EB=B3=B4=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/audition/role/AdminAuditionRoleService.kt         | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

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
index 37870ae..5e5b24a 100644
--- 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
@@ -50,11 +50,13 @@ class AdminAuditionRoleService(
         val auditionRole = repository.findByIdOrNull(id = request.id)
             ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
 
-        if (!request.name.isNullOrBlank() && request.name.length > 2) {
+        if (!request.name.isNullOrBlank()) {
+            if (request.name.length < 2) throw SodaException("배역 이름은 최소 2글자 입니다")
             auditionRole.name = request.name
         }
 
-        if (!request.information.isNullOrBlank() && request.information.length >= 10) {
+        if (!request.information.isNullOrBlank()) {
+            if (request.information.length < 10) throw SodaException("오디션 배역 정보는 최소 10글자 입니다")
             auditionRole.information = request.information
         }
 

From f77b5f67d07773d5a7c220b965461a3561a6fe02 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Mon, 30 Dec 2024 20:29:28 +0900
Subject: [PATCH 19/40] =?UTF-8?q?=EC=95=B1=20API=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/AuditionController.kt   | 30 +++++++
 .../sodalive/audition/AuditionRepository.kt   | 78 +++++++++++++++++++
 .../sodalive/audition/AuditionService.kt      | 14 ++++
 .../audition/GetAuditionListResponse.kt       | 16 ++++
 4 files changed, 138 insertions(+)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionListResponse.kt

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..ee3e7e4
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt
@@ -0,0 +1,30 @@
+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.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
+            )
+        )
+    }
+}
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..9fa812d
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
@@ -0,0 +1,78 @@
+package kr.co.vividnext.sodalive.audition
+
+import com.querydsl.jpa.impl.JPAQueryFactory
+import kr.co.vividnext.sodalive.audition.QAudition.audition
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface AuditionRepository : JpaRepository<Audition, Long>, AuditionQueryRepository
+
+interface AuditionQueryRepository {
+    fun getInProgressAuditionCount(isAdult: Boolean): Int
+    fun getCompletedAuditionCount(isAdult: Boolean): Int
+    fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem>
+}
+
+class AuditionQueryRepositoryImpl(
+    private val queryFactory: JPAQueryFactory,
+
+    @Value("\${cloud.aws.cloud-front.host}")
+    private val cloudFrontHost: String
+) : AuditionQueryRepository {
+    override fun getInProgressAuditionCount(isAdult: Boolean): Int {
+        var where = audition.isActive.isTrue
+            .and(audition.status.eq(AuditionStatus.IN_PROGRESS))
+
+        if (!isAdult) {
+            where = where.and(audition.isAdult.isFalse)
+        }
+
+        return queryFactory
+            .select(audition.id)
+            .from(audition)
+            .where(where)
+            .fetch()
+            .size
+    }
+
+    override fun getCompletedAuditionCount(isAdult: Boolean): Int {
+        var where = audition.isActive.isTrue
+            .and(audition.status.eq(AuditionStatus.COMPLETED))
+
+        if (!isAdult) {
+            where = where.and(audition.isAdult.isFalse)
+        }
+
+        return queryFactory
+            .select(audition.id)
+            .from(audition)
+            .where(where)
+            .fetch()
+            .size
+    }
+
+    override fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem> {
+        var where = audition.isActive.isTrue
+            .and(
+                audition.status.eq(AuditionStatus.COMPLETED)
+                    .and(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)
+            .fetch()
+    }
+}
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..cde6ad5
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt
@@ -0,0 +1,14 @@
+package kr.co.vividnext.sodalive.audition
+
+import org.springframework.stereotype.Service
+
+@Service
+class AuditionService(private val repository: AuditionRepository) {
+    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)
+    }
+}
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<GetAuditionListItem>
+)
+
+data class GetAuditionListItem @QueryProjection constructor(
+    val id: Long,
+    val title: String,
+    val imageUrl: String,
+    val isOff: Boolean
+)

From b56b2e15aff644dfb8f91709a7aa0df010c18d55 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Mon, 30 Dec 2024 20:49:03 +0900
Subject: [PATCH 20/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=BA=90?=
 =?UTF-8?q?=EB=A6=AD=ED=84=B0(=EB=B0=B0=EC=97=AD)=20=EA=B4=80=EB=A6=AC?=
 =?UTF-8?q?=EC=9E=90=20API=20-=20=EA=B4=80=EB=A6=AC=EC=9E=90=EB=A7=8C=20?=
 =?UTF-8?q?=EC=8B=A4=ED=96=89=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?=
 =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/admin/audition/role/AdminAuditionRoleController.kt | 2 ++
 1 file changed, 2 insertions(+)

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
index f13a248..947f132 100644
--- 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
@@ -2,6 +2,7 @@ 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
@@ -12,6 +13,7 @@ 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

From ddd552deb4353effd998dcd683a9ba23d6a12276 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Mon, 30 Dec 2024 23:42:25 +0900
Subject: [PATCH 21/40] =?UTF-8?q?=EC=95=B1=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EC=83=81=EC=84=B8=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/AuditionController.kt   | 11 +++++
 .../sodalive/audition/AuditionRepository.kt   | 16 +++++++
 .../sodalive/audition/AuditionService.kt      | 19 +++++++-
 .../audition/GetAuditionDetailResponse.kt     | 25 +++++++++++
 .../audition/role/AuditionRoleRepository.kt   | 43 +++++++++++++++++++
 5 files changed, 113 insertions(+), 1 deletion(-)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/GetAuditionDetailResponse.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt
index ee3e7e4..ce4593c 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionController.kt
@@ -6,6 +6,7 @@ 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
 
@@ -27,4 +28,14 @@ class AuditionController(private val service: AuditionService) {
             )
         )
     }
+
+    @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
index 9fa812d..8bb8360 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
@@ -11,6 +11,7 @@ interface AuditionQueryRepository {
     fun getInProgressAuditionCount(isAdult: Boolean): Int
     fun getCompletedAuditionCount(isAdult: Boolean): Int
     fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem>
+    fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData
 }
 
 class AuditionQueryRepositoryImpl(
@@ -75,4 +76,19 @@ class AuditionQueryRepositoryImpl(
             .where(where)
             .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/AuditionService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt
index cde6ad5..2f6286c 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionService.kt
@@ -1,9 +1,13 @@
 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) {
+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)
@@ -11,4 +15,17 @@ class AuditionService(private val repository: AuditionRepository) {
 
         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/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<GetAuditionRoleListData>
+)
+
+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/role/AuditionRoleRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt
new file mode 100644
index 0000000..b46f4df
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt
@@ -0,0 +1,43 @@
+package kr.co.vividnext.sodalive.audition.role
+
+import com.querydsl.jpa.impl.JPAQueryFactory
+import kr.co.vividnext.sodalive.audition.AuditionRole
+import kr.co.vividnext.sodalive.audition.AuditionStatus
+import kr.co.vividnext.sodalive.audition.GetAuditionRoleListData
+import kr.co.vividnext.sodalive.audition.QAudition.audition
+import kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
+import kr.co.vividnext.sodalive.audition.QGetAuditionRoleListData
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface AuditionRoleRepository : JpaRepository<AuditionRole, Long>, AuditionRoleQueryRepository
+
+interface AuditionRoleQueryRepository {
+    fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData>
+}
+
+class AuditionRoleQueryRepositoryImpl(
+    private val queryFactory: JPAQueryFactory,
+
+    @Value("\${cloud.aws.cloud-front.host}")
+    private val cloudfrontHost: String
+) : AuditionRoleQueryRepository {
+    override fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData> {
+        return queryFactory
+            .select(
+                QGetAuditionRoleListData(
+                    auditionRole.id,
+                    auditionRole.name,
+                    auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
+                    auditionRole.status.eq(AuditionStatus.COMPLETED)
+                )
+            )
+            .from(auditionRole)
+            .innerJoin(auditionRole.audition, audition)
+            .where(
+                audition.id.eq(auditionId),
+                auditionRole.isActive.isTrue
+            )
+            .fetch()
+    }
+}

From affbb3eba323f06effadbc7b8d018a4928fc6e22 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Tue, 31 Dec 2024 02:46:19 +0900
Subject: [PATCH 22/40] =?UTF-8?q?=EC=95=B1=20-=20=EB=B0=B0=EC=97=AD=20?=
 =?UTF-8?q?=EC=83=81=EC=84=B8=20API,=20=EC=A7=80=EC=9B=90=20=EB=A6=AC?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../applicant/AuditionApplicantController.kt  | 34 +++++++
 .../applicant/AuditionApplicantRepository.kt  | 93 +++++++++++++++++++
 .../applicant/AuditionApplicantService.kt     | 26 ++++++
 .../applicant/AuditionApplicantSortType.kt    |  5 +
 .../GetAuditionApplicantListResponse.kt       | 16 ++++
 .../audition/role/AuditionRoleController.kt   | 29 ++++++
 .../audition/role/AuditionRoleRepository.kt   | 22 +++++
 .../audition/role/AuditionRoleService.kt      | 23 +++++
 .../role/GetAuditionRoleDetailResponse.kt     | 32 +++++++
 9 files changed, 280 insertions(+)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantController.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantSortType.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/GetAuditionApplicantListResponse.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleController.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleService.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/role/GetAuditionRoleDetailResponse.kt

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..ff03e54
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantController.kt
@@ -0,0 +1,34 @@
+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.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
+import org.springframework.web.bind.annotation.RestController
+
+@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()
+            )
+        )
+    }
+}
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..23f88ac
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
@@ -0,0 +1,93 @@
+package kr.co.vividnext.sodalive.audition.applicant
+
+import com.querydsl.jpa.impl.JPAQueryFactory
+import kr.co.vividnext.sodalive.audition.AuditionApplicant
+import kr.co.vividnext.sodalive.audition.QAuditionApplicant.auditionApplicant
+import kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
+import kr.co.vividnext.sodalive.audition.QAuditionVote.auditionVote
+import kr.co.vividnext.sodalive.member.QMember.member
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface AuditionApplicantRepository : JpaRepository<AuditionApplicant, Long>, AuditionApplicantQueryRepository
+
+interface AuditionApplicantQueryRepository {
+    fun isAlreadyApplicant(auditionRoleId: Long, memberId: Long): Boolean
+    fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int
+    fun getAuditionApplicantList(
+        auditionRoleId: Long,
+        sortType: AuditionApplicantSortType,
+        offset: Long,
+        limit: Long
+    ): List<GetAuditionRoleApplicantItem>
+}
+
+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),
+                auditionRole.isActive.isTrue
+            )
+            .fetch()
+            .size
+    }
+
+    override fun getAuditionApplicantList(
+        auditionRoleId: Long,
+        sortType: AuditionApplicantSortType,
+        offset: Long,
+        limit: Long
+    ): List<GetAuditionRoleApplicantItem> {
+        val orderBy = if (sortType == AuditionApplicantSortType.LIKES) {
+            auditionVote.id.count().desc()
+        } else {
+            auditionApplicant.id.desc()
+        }
+
+        return queryFactory
+            .select(
+                QGetAuditionRoleApplicantItem(
+                    auditionApplicant.id,
+                    member.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),
+                auditionRole.isActive.isTrue
+            )
+            .groupBy(auditionApplicant.id)
+            .orderBy(orderBy)
+            .fetch()
+    }
+}
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..edc1c94
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
@@ -0,0 +1,26 @@
+package kr.co.vividnext.sodalive.audition.applicant
+
+import org.springframework.stereotype.Service
+
+@Service
+class AuditionApplicantService(private val repository: AuditionApplicantRepository) {
+    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
+        )
+    }
+}
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..d86ff49
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/GetAuditionApplicantListResponse.kt
@@ -0,0 +1,16 @@
+package kr.co.vividnext.sodalive.audition.applicant
+
+import com.querydsl.core.annotations.QueryProjection
+
+data class GetAuditionApplicantListResponse(
+    val totalCount: Int,
+    val items: List<GetAuditionRoleApplicantItem>
+)
+
+data class GetAuditionRoleApplicantItem @QueryProjection constructor(
+    val applicantId: Long,
+    val 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
index b46f4df..735dffc 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt
@@ -14,6 +14,7 @@ interface AuditionRoleRepository : JpaRepository<AuditionRole, Long>, AuditionRo
 
 interface AuditionRoleQueryRepository {
     fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData>
+    fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailData?
 }
 
 class AuditionRoleQueryRepositoryImpl(
@@ -40,4 +41,25 @@ class AuditionRoleQueryRepositoryImpl(
             )
             .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
+    )
+}

From 00c306475cba85de6d46ce2eb038583008d6f11c Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Tue, 31 Dec 2024 03:37:00 +0900
Subject: [PATCH 23/40] =?UTF-8?q?=EC=95=B1=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API=20-=20=EC=83=81?=
 =?UTF-8?q?=ED=83=9C=20=EC=A0=95=EB=A0=AC=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../kr/co/vividnext/sodalive/audition/AuditionRepository.kt      | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
index 8bb8360..8602388 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
@@ -74,6 +74,7 @@ class AuditionQueryRepositoryImpl(
             )
             .from(audition)
             .where(where)
+            .orderBy(audition.status.desc())
             .fetch()
     }
 

From 9315447618fd83b567f17578a8f93c0a32712549 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Tue, 31 Dec 2024 07:26:25 +0900
Subject: [PATCH 24/40] =?UTF-8?q?=EC=95=B1=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API=20-=20offset,=20l?=
 =?UTF-8?q?imit=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../kr/co/vividnext/sodalive/audition/AuditionRepository.kt   | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
index 8602388..1c63e6f 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionRepository.kt
@@ -56,7 +56,7 @@ class AuditionQueryRepositoryImpl(
         var where = audition.isActive.isTrue
             .and(
                 audition.status.eq(AuditionStatus.COMPLETED)
-                    .and(audition.status.eq(AuditionStatus.IN_PROGRESS))
+                    .or(audition.status.eq(AuditionStatus.IN_PROGRESS))
             )
 
         if (!isAdult) {
@@ -74,6 +74,8 @@ class AuditionQueryRepositoryImpl(
             )
             .from(audition)
             .where(where)
+            .offset(offset)
+            .limit(limit)
             .orderBy(audition.status.desc())
             .fetch()
     }

From 8385800e48ece098e8abfe0c17b0af0c528e4e8e Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Tue, 31 Dec 2024 09:12:26 +0900
Subject: [PATCH 25/40] =?UTF-8?q?=EC=95=B1=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EC=83=81=EC=84=B8=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?=
 =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20-=20status=20=EB=82=B4=EB=A6=BC?=
 =?UTF-8?q?=EC=B0=A8=EC=88=9C=20=EC=A0=95=EB=A0=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vividnext/sodalive/audition/role/AuditionRoleRepository.kt   | 1 +
 1 file changed, 1 insertion(+)

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
index 735dffc..b914f58 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/role/AuditionRoleRepository.kt
@@ -39,6 +39,7 @@ class AuditionRoleQueryRepositoryImpl(
                 audition.id.eq(auditionId),
                 auditionRole.isActive.isTrue
             )
+            .orderBy(auditionRole.status.desc(), auditionRole.id.desc())
             .fetch()
     }
 

From 96f571e0c439a7e79399a71c04ed50b94ae7b471 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Thu, 2 Jan 2025 19:32:31 +0900
Subject: [PATCH 26/40] =?UTF-8?q?=EC=95=B1=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EC=A7=80=EC=9B=90=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/AuditionApplicant.kt    |  2 +-
 .../applicant/ApplyAuditionRoleRequest.kt     |  6 +++
 .../applicant/AuditionApplicantController.kt  | 21 ++++++++
 .../applicant/AuditionApplicantService.kt     | 50 ++++++++++++++++++-
 4 files changed, 77 insertions(+), 2 deletions(-)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/ApplyAuditionRoleRequest.kt

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt
index e485407..21dc568 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt
@@ -9,8 +9,8 @@ import javax.persistence.ManyToOne
 
 @Entity
 data class AuditionApplicant(
-    val voicePath: String,
     val phoneNumber: String,
+    var voicePath: String? = null,
     val isActive: Boolean = true
 ) : BaseEntity() {
     @ManyToOne(fetch = FetchType.LAZY)
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
index ff03e54..9caeb2d 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantController.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantController.kt
@@ -6,9 +6,12 @@ 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")
@@ -31,4 +34,22 @@ class AuditionApplicantController(private val service: AuditionApplicantService)
             )
         )
     }
+
+    @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/AuditionApplicantService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
index edc1c94..d27f7ca 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
@@ -1,9 +1,30 @@
 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) {
+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,
@@ -23,4 +44,31 @@ class AuditionApplicantService(private val repository: AuditionApplicantReposito
             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 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.id}/$contentFileName",
+            metadata = metadata
+        )
+        applicant.voicePath = contentPath
+    }
 }

From 7a395a9906dcc9dbfc5a9d90090207d6109c1874 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Thu, 2 Jan 2025 22:46:57 +0900
Subject: [PATCH 27/40] =?UTF-8?q?=EC=95=B1=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=EC=A7=80=EC=9B=90=20API=20-=20=EA=B8=B0=EC=A1=B4?=
 =?UTF-8?q?=EC=97=90=20=EC=A7=80=EC=9B=90=ED=95=9C=20=EB=82=B4=EC=97=AD?=
 =?UTF-8?q?=EC=9D=B4=20=EC=9E=88=EC=9C=BC=EB=A9=B4=20false=20=EC=B2=98?=
 =?UTF-8?q?=EB=A6=AC=20=ED=9B=84=20=EC=A7=80=EC=9B=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/AuditionApplicant.kt          |  2 +-
 .../applicant/AuditionApplicantRepository.kt        | 13 +++++++++++++
 .../audition/applicant/AuditionApplicantService.kt  | 10 ++++++++++
 3 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt
index 21dc568..fcba813 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/AuditionApplicant.kt
@@ -11,7 +11,7 @@ import javax.persistence.ManyToOne
 data class AuditionApplicant(
     val phoneNumber: String,
     var voicePath: String? = null,
-    val isActive: Boolean = true
+    var isActive: Boolean = true
 ) : BaseEntity() {
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "role_id", nullable = false)
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
index 23f88ac..c20599f 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
@@ -20,6 +20,8 @@ interface AuditionApplicantQueryRepository {
         offset: Long,
         limit: Long
     ): List<GetAuditionRoleApplicantItem>
+
+    fun findActiveApplicantByMemberIdAndRoleId(memberId: Long, roleId: Long): AuditionApplicant?
 }
 
 class AuditionApplicantQueryRepositoryImpl(
@@ -90,4 +92,15 @@ class AuditionApplicantQueryRepositoryImpl(
             .orderBy(orderBy)
             .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
index d27f7ca..971bd01 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
@@ -53,6 +53,16 @@ class AuditionApplicantService(
         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

From cd0c066978f8e84275129782e5598455726ccb63 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 00:09:53 +0900
Subject: [PATCH 28/40] =?UTF-8?q?=EC=95=B1=20-=20=EC=98=A4=EB=94=94?=
 =?UTF-8?q?=EC=85=98=20=ED=88=AC=ED=91=9C=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../audition/vote/AuditionVoteController.kt   | 33 ++++++++++
 .../audition/vote/AuditionVoteRepository.kt   | 40 ++++++++++++
 .../audition/vote/AuditionVoteService.kt      | 64 +++++++++++++++++++
 .../vote/VoteAuditionApplicantRequest.kt      |  7 ++
 .../co/vividnext/sodalive/can/use/CanUsage.kt |  3 +-
 5 files changed, 146 insertions(+), 1 deletion(-)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteController.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteRepository.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt

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..2a5d61a
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteRepository.kt
@@ -0,0 +1,40 @@
+package kr.co.vividnext.sodalive.audition.vote
+
+import com.querydsl.jpa.impl.JPAQueryFactory
+import kr.co.vividnext.sodalive.audition.AuditionVote
+import kr.co.vividnext.sodalive.audition.QAuditionVote.auditionVote
+import org.springframework.data.jpa.repository.JpaRepository
+import java.time.LocalDateTime
+
+interface AuditionVoteRepository : JpaRepository<AuditionVote, Long>, AuditionVoteQueryRepository
+
+interface AuditionVoteQueryRepository {
+    fun countByMemberIdAndApplicantIdAndVoteDateRange(
+        memberId: Long,
+        applicantId: Long,
+        startDate: LocalDateTime,
+        endDate: LocalDateTime
+    ): Int
+}
+
+class AuditionVoteQueryRepositoryImpl(
+    private val queryFactory: JPAQueryFactory
+) : AuditionVoteQueryRepository {
+    override fun countByMemberIdAndApplicantIdAndVoteDateRange(
+        memberId: Long,
+        applicantId: Long,
+        startDate: LocalDateTime,
+        endDate: LocalDateTime
+    ): Int {
+        return queryFactory
+            .select(auditionVote.id)
+            .from(auditionVote)
+            .where(
+                auditionVote.member.id.eq(memberId)
+                    .and(auditionVote.applicant.id.eq(applicantId))
+                    .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..4496ead
--- /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.countByMemberIdAndApplicantIdAndVoteDateRange(
+            memberId = member.id!!,
+            applicantId = applicantId,
+            startDate = startDate,
+            endDate = endDate
+        )
+
+        if (voteCount > 10) {
+            throw SodaException("오늘 해당 지원자에게 할 수 있는 최대 투표수를 초과하였습니다.\n내일 다시 투표해 주세요.")
+        }
+
+        if (voteCount > 0) {
+            canPaymentService.spendCan(
+                memberId = member.id!!,
+                needCan = 1,
+                canUsage = CanUsage.AUDITION_VOTE,
+                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/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
 }

From 82b109e3bd52c8158b71586addc957095e99a2e1 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 00:42:54 +0900
Subject: [PATCH 29/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=ED=88=AC?=
 =?UTF-8?q?=ED=91=9C=20API=20-=20=ED=88=AC=ED=91=9C=EC=8B=9C=20=EC=96=B4?=
 =?UTF-8?q?=EB=96=A4=20=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80=EC=9B=90?=
 =?UTF-8?q?=EC=97=90=20=ED=88=AC=ED=91=9C=ED=96=88=EB=8A=94=EC=A7=80=20?=
 =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20-=20=EC=BA=94=20=EC=82=AC=EC=9A=A9?=
 =?UTF-8?q?=EB=82=B4=EC=97=AD=EC=97=90=20"[=EC=98=A4=EB=94=94=EC=85=98=20?=
 =?UTF-8?q?=ED=88=AC=ED=91=9C]=20=EB=8B=89=EB=84=A4=EC=9E=84"=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vividnext/sodalive/audition/vote/AuditionVoteService.kt  | 1 +
 src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt   | 1 +
 .../co/vividnext/sodalive/can/payment/CanPaymentService.kt   | 4 ++++
 src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt   | 5 +++++
 4 files changed, 11 insertions(+)

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
index 4496ead..a897527 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt
@@ -52,6 +52,7 @@ class AuditionVoteService(
                 memberId = member.id!!,
                 needCan = 1,
                 canUsage = CanUsage.AUDITION_VOTE,
+                auditionApplicant = applicant,
                 container = container
             )
         }
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..988cee3 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?.member?.nickname ?: ""}"
                 }
 
                 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..a90f74a 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,8 @@ class CanPaymentService(
             useCan.member = member
         } else if (canUsage == CanUsage.ALARM_SLOT) {
             useCan.member = member
+        } else if (canUsage == CanUsage.AUDITION_VOTE && auditionApplicant != null) {
+            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/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<UseCanCalculate> = mutableListOf()
 }

From b10c102f9401d1c2837e3eaa8b48be18611a3b29 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 01:03:38 +0900
Subject: [PATCH 30/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=ED=88=AC?=
 =?UTF-8?q?=ED=91=9C=20API=20-=20=ED=88=AC=ED=91=9C=EC=8B=9C=20=EC=96=B4?=
 =?UTF-8?q?=EB=96=A4=20=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80=EC=9B=90?=
 =?UTF-8?q?=EC=97=90=20=ED=88=AC=ED=91=9C=ED=96=88=EB=8A=94=EC=A7=80=20?=
 =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20-=20=EC=BA=94=20=EC=82=AC=EC=9A=A9?=
 =?UTF-8?q?=EB=82=B4=EC=97=AD=EC=97=90=20"[=EC=98=A4=EB=94=94=EC=85=98=20?=
 =?UTF-8?q?=ED=88=AC=ED=91=9C]=20=EB=8B=89=EB=84=A4=EC=9E=84"=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt    | 1 +
 1 file changed, 1 insertion(+)

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 a90f74a..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
@@ -103,6 +103,7 @@ class CanPaymentService(
         } 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!!

From c8f96a10f0a4083d5583936f5a6341a5dff9ebde Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 01:29:42 +0900
Subject: [PATCH 31/40] =?UTF-8?q?=EC=BA=94=20=EC=82=AC=EC=9A=A9=EB=82=B4?=
 =?UTF-8?q?=EC=97=AD=20-=20=EC=98=A4=EB=94=94=EC=85=98=20=ED=88=AC?=
 =?UTF-8?q?=ED=91=9C=20-=20"[=EC=98=A4=EB=94=94=EC=85=98=20=ED=88=AC?=
 =?UTF-8?q?=ED=91=9C]=20=EC=98=A4=EB=94=94=EC=85=98=EB=AA=85"=EC=9C=BC?=
 =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 988cee3..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,7 +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?.member?.nickname ?: ""}"
+                    CanUsage.AUDITION_VOTE -> "[오디션 투표] ${it.auditionApplicant?.role?.audition?.title ?: ""}"
                 }
 
                 val createdAt = it.createdAt!!

From 80841fe5439005574849a6510f14e0b5c310a69a Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 07:53:16 +0900
Subject: [PATCH 32/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80?=
 =?UTF-8?q?=EC=9B=90=20-=20=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80?=
 =?UTF-8?q?=EC=9B=90=20=ED=8C=8C=EC=9D=BC=20=EC=A0=80=EC=9E=A5=20=EA=B2=BD?=
 =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/applicant/AuditionApplicantService.kt     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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
index 971bd01..92c410e 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantService.kt
@@ -76,7 +76,7 @@ class AuditionApplicantService(
         val contentPath = s3Uploader.upload(
             inputStream = contentFile.inputStream,
             bucket = bucket,
-            filePath = "audition/${applicant.id}/$contentFileName",
+            filePath = "audition/applicant/${applicant.id}/$contentFileName",
             metadata = metadata
         )
         applicant.voicePath = contentPath

From 460196dc4ded83571a871af66a9fe1ad7c65a780 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 08:04:58 +0900
Subject: [PATCH 33/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80?=
 =?UTF-8?q?=EC=9B=90=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20-=20=ED=8E=98?=
 =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EC=A0=81=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/applicant/AuditionApplicantRepository.kt  | 2 ++
 1 file changed, 2 insertions(+)

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
index c20599f..3c83256 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
@@ -90,6 +90,8 @@ class AuditionApplicantQueryRepositoryImpl(
             )
             .groupBy(auditionApplicant.id)
             .orderBy(orderBy)
+            .offset(offset)
+            .limit(limit)
             .fetch()
     }
 

From 64d9f3e36264ce71f68d76d83e1db671b822d322 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 08:33:52 +0900
Subject: [PATCH 34/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80?=
 =?UTF-8?q?=EC=9B=90=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20-=20=EB=B9=84?=
 =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EB=90=9C=20=EB=8D=B0=EC=9D=B4?=
 =?UTF-8?q?=ED=84=B0=EB=8A=94=20=EC=A1=B0=ED=9A=8C=EB=90=98=EC=A7=80=20?=
 =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../audition/applicant/AuditionApplicantRepository.kt         | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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
index 3c83256..d2f766c 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
@@ -52,7 +52,7 @@ class AuditionApplicantQueryRepositoryImpl(
             .innerJoin(auditionApplicant.role, auditionRole)
             .where(
                 auditionRole.id.eq(auditionRoleId),
-                auditionRole.isActive.isTrue
+                auditionApplicant.isActive.isTrue
             )
             .fetch()
             .size
@@ -86,7 +86,7 @@ class AuditionApplicantQueryRepositoryImpl(
             .leftJoin(auditionVote).on(auditionApplicant.id.eq(auditionVote.applicant.id))
             .where(
                 auditionRole.id.eq(auditionRoleId),
-                auditionRole.isActive.isTrue
+                auditionApplicant.isActive.isTrue
             )
             .groupBy(auditionApplicant.id)
             .orderBy(orderBy)

From 1ddd40948e3b68ba7ff796f3c8ef624d8f04b1a5 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 13:00:02 +0900
Subject: [PATCH 35/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=ED=88=AC?=
 =?UTF-8?q?=ED=91=9C=20-=20=ED=9A=9F=EC=88=98=20=EA=B3=84=EC=82=B0=20?=
 =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95=20-=20=EC=98=A4?=
 =?UTF-8?q?=EB=94=94=EC=85=98=20=EC=A7=80=EC=9B=90=EC=9E=90=EB=B3=84=20?=
 =?UTF-8?q?=ED=95=98=EB=A3=A8=2010=EA=B0=9C=20->=20=EC=A0=84=EC=B2=B4=20?=
 =?UTF-8?q?=ED=88=AC=ED=91=9C=20=ED=9A=9F=EC=88=98=20=ED=95=98=EB=A3=A8=20?=
 =?UTF-8?q?10=EA=B0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/vote/AuditionVoteRepository.kt       | 7 ++-----
 .../sodalive/audition/vote/AuditionVoteService.kt          | 5 ++---
 2 files changed, 4 insertions(+), 8 deletions(-)

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
index 2a5d61a..584ed3d 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteRepository.kt
@@ -9,9 +9,8 @@ import java.time.LocalDateTime
 interface AuditionVoteRepository : JpaRepository<AuditionVote, Long>, AuditionVoteQueryRepository
 
 interface AuditionVoteQueryRepository {
-    fun countByMemberIdAndApplicantIdAndVoteDateRange(
+    fun countByMemberIdAndVoteDateRange(
         memberId: Long,
-        applicantId: Long,
         startDate: LocalDateTime,
         endDate: LocalDateTime
     ): Int
@@ -20,9 +19,8 @@ interface AuditionVoteQueryRepository {
 class AuditionVoteQueryRepositoryImpl(
     private val queryFactory: JPAQueryFactory
 ) : AuditionVoteQueryRepository {
-    override fun countByMemberIdAndApplicantIdAndVoteDateRange(
+    override fun countByMemberIdAndVoteDateRange(
         memberId: Long,
-        applicantId: Long,
         startDate: LocalDateTime,
         endDate: LocalDateTime
     ): Int {
@@ -31,7 +29,6 @@ class AuditionVoteQueryRepositoryImpl(
             .from(auditionVote)
             .where(
                 auditionVote.member.id.eq(memberId)
-                    .and(auditionVote.applicant.id.eq(applicantId))
                     .and(auditionVote.createdAt.between(startDate, endDate))
             )
             .fetch()
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
index a897527..34261ea 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt
@@ -36,15 +36,14 @@ class AuditionVoteService(
         val startDate = startOfDayClient.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
         val endDate = endOfDayClient.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
 
-        val voteCount = repository.countByMemberIdAndApplicantIdAndVoteDateRange(
+        val voteCount = repository.countByMemberIdAndVoteDateRange(
             memberId = member.id!!,
-            applicantId = applicantId,
             startDate = startDate,
             endDate = endDate
         )
 
         if (voteCount > 10) {
-            throw SodaException("오늘 해당 지원자에게 할 수 있는 최대 투표수를 초과하였습니다.\n내일 다시 투표해 주세요.")
+            throw SodaException("오늘 응원은 여기까지!\n하루 최대 10회까지 응원이 가능합니다.\n내일 다시 이용해주세요.")
         }
 
         if (voteCount > 0) {

From 354fbf7e29a5bdf1e6df7f2a4253add79e47dbe4 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Fri, 3 Jan 2025 23:40:26 +0900
Subject: [PATCH 36/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=A7=80?=
 =?UTF-8?q?=EC=9B=90=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20-=20=EC=A7=80?=
 =?UTF-8?q?=EC=9B=90=EC=9E=90=20memberId=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/audition/applicant/AuditionApplicantRepository.kt   | 1 +
 .../audition/applicant/GetAuditionApplicantListResponse.kt       | 1 +
 2 files changed, 2 insertions(+)

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
index d2f766c..92af3f3 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantRepository.kt
@@ -74,6 +74,7 @@ class AuditionApplicantQueryRepositoryImpl(
             .select(
                 QGetAuditionRoleApplicantItem(
                     auditionApplicant.id,
+                    member.id,
                     member.nickname,
                     member.profileImage.prepend("/").prepend(cloudFrontHost),
                     auditionApplicant.voicePath.prepend("/").prepend(cloudFrontHost),
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
index d86ff49..188e17b 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/GetAuditionApplicantListResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/applicant/GetAuditionApplicantListResponse.kt
@@ -9,6 +9,7 @@ data class GetAuditionApplicantListResponse(
 
 data class GetAuditionRoleApplicantItem @QueryProjection constructor(
     val applicantId: Long,
+    val memberId: Long,
     val nickname: String,
     val profileImageUrl: String,
     val voiceUrl: String,

From eb36313c9b057b2eab77a2d591cbbe30b1a75be0 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 4 Jan 2025 00:43:10 +0900
Subject: [PATCH 37/40] =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4?=
 =?UTF-8?q?=ED=84=B0=20=ED=94=84=EB=A1=9C=ED=95=84=20API=20-=20=EB=9E=AD?=
 =?UTF-8?q?=ED=82=B9,=20=EC=B6=94=EC=B2=9C=20=ED=81=AC=EB=A6=AC,=20?=
 =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C,=20=EC=BD=98=ED=85=90=EC=B8=A0,=20?=
 =?UTF-8?q?=EC=8B=9C=EB=A6=AC=EC=A6=88,=20=EC=BB=A4=EB=AE=A4=EB=8B=88?=
 =?UTF-8?q?=ED=8B=B0,=20=ED=99=9C=EB=8F=99=EC=9A=94=EC=95=BD=EC=9D=84=20?=
 =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0=EC=9D=B8=20?=
 =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EC=97=90=EB=A7=8C=20=EC=A1=B0=ED=9A=8C?=
 =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/explorer/ExplorerService.kt      | 112 ++++++++++++------
 .../explorer/GetCreatorProfileResponse.kt     |   3 +-
 2 files changed, 75 insertions(+), 40 deletions(-)

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..a7c3a0e 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt
@@ -144,12 +144,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 +164,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 +222,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 +273,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,
+            isCreator = 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..5ff9ed5 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<GetSeriesListResponse.SeriesListItem>,
-    val isBlock: Boolean
+    val isBlock: Boolean,
+    val isCreator: Boolean
 )
 
 data class GetCreatorActivitySummary(

From 47dfaec2262b515e91745eac9e7bfb8fb429eeda Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sat, 4 Jan 2025 01:07:37 +0900
Subject: [PATCH 38/40] =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4?=
 =?UTF-8?q?=ED=84=B0=20=ED=94=84=EB=A1=9C=ED=95=84=20API=20-=20isCreator?=
 =?UTF-8?q?=EB=A5=BC=20isCreatorRole=EC=9D=B4=EB=9D=BC=EB=8A=94=20?=
 =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-?=
 =?UTF-8?q?=20=EB=82=98=ED=83=80=EB=82=B4=EB=8A=94=20=EA=B0=92=EC=9D=80=20?=
 =?UTF-8?q?=EB=8F=99=EC=9D=BC=ED=95=98=EB=8B=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt | 2 +-
 .../co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

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 a7c3a0e..1ce31f5 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt
@@ -276,7 +276,7 @@ class ExplorerService(
             activitySummary = activitySummary,
             seriesList = seriesList,
             isBlock = isBlock,
-            isCreator = isCreator
+            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 5ff9ed5..5e80a77 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt
@@ -16,7 +16,7 @@ data class GetCreatorProfileResponse(
     val activitySummary: GetCreatorActivitySummary,
     val seriesList: List<GetSeriesListResponse.SeriesListItem>,
     val isBlock: Boolean,
-    val isCreator: Boolean
+    val isCreatorRole: Boolean
 )
 
 data class GetCreatorActivitySummary(

From 824cd2f3ea4f7a43fbe3e161ee74770a88fa7dc2 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sun, 5 Jan 2025 15:27:03 +0900
Subject: [PATCH 39/40] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20-=20endDate?=
 =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../admin/audition/AdminAuditionRepository.kt | 23 -------------------
 .../admin/audition/AdminAuditionService.kt    | 12 ----------
 .../admin/audition/CreateAuditionRequest.kt   | 15 ------------
 .../admin/audition/GetAuditionListResponse.kt |  1 -
 .../admin/audition/UpdateAuditionRequest.kt   |  1 -
 .../vividnext/sodalive/audition/Audition.kt   |  2 --
 6 files changed, 54 deletions(-)

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
index a457e59..dbdbe16 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionRepository.kt
@@ -1,16 +1,11 @@
 package kr.co.vividnext.sodalive.admin.audition
 
-import com.querydsl.core.types.dsl.CaseBuilder
-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<Audition, Long>, AdminAuditionQueryRepository
@@ -33,10 +28,6 @@ class AdminAuditionQueryRepositoryImpl(
                 QGetAuditionListItem(
                     audition.id,
                     audition.title,
-                    CaseBuilder()
-                        .`when`(audition.endDate.isNotNull)
-                        .then(getFormattedDate(audition.endDate))
-                        .otherwise(""),
                     audition.imagePath.prepend("/").prepend(coverImageHost),
                     audition.isAdult,
                     audition.information,
@@ -75,18 +66,4 @@ class AdminAuditionQueryRepositoryImpl(
             .where(audition.id.eq(auditionId))
             .fetchFirst()
     }
-
-    private fun getFormattedDate(dateTimePath: DateTimePath<LocalDateTime>): 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
index 774b5b8..b0ad818 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt
@@ -11,9 +11,6 @@ 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(
@@ -68,15 +65,6 @@ class AdminAuditionService(
             audition.status = request.status
         }
 
-        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
         }
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
index 647fa34..ccf7a10 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/CreateAuditionRequest.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/CreateAuditionRequest.kt
@@ -2,15 +2,11 @@ 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 {
@@ -24,21 +20,10 @@ data class CreateAuditionRequest(
     }
 
     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
index 4858790..8d63ef2 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/GetAuditionListResponse.kt
@@ -11,7 +11,6 @@ data class GetAuditionListResponse(
 data class GetAuditionListItem @QueryProjection constructor(
     val id: Long,
     val title: String,
-    val endDate: String,
     val imageUrl: String,
     val isAdult: Boolean,
     val information: 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
index 540e446..a90be2d 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/UpdateAuditionRequest.kt
@@ -7,7 +7,6 @@ data class UpdateAuditionRequest(
     val title: String? = null,
     val information: String? = null,
     val isAdult: Boolean? = null,
-    val endDateString: String? = null,
     val status: AuditionStatus? = 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
index 24e2727..ce29dd6 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/Audition.kt
@@ -1,7 +1,6 @@
 package kr.co.vividnext.sodalive.audition
 
 import kr.co.vividnext.sodalive.common.BaseEntity
-import java.time.LocalDateTime
 import javax.persistence.Column
 import javax.persistence.Entity
 import javax.persistence.EnumType
@@ -13,7 +12,6 @@ data class Audition(
     @Column(columnDefinition = "TEXT")
     var information: String,
     var isAdult: Boolean = false,
-    var endDate: LocalDateTime? = null,
     // 원작 URL
     var originalWorkUrl: String? = null,
     @Enumerated(value = EnumType.STRING)

From c4d9d503ac604129fdee12c75923fadb2c263fec Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Sun, 5 Jan 2025 15:35:26 +0900
Subject: [PATCH 40/40] =?UTF-8?q?=ED=83=90=EC=83=89=EC=97=90=20=EC=9E=88?=
 =?UTF-8?q?=EB=8A=94=20=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0=20?=
 =?UTF-8?q?=EB=9E=AD=ED=82=B9=EA=B3=BC=20=EB=8F=99=EC=9D=BC=ED=95=9C=20?=
 =?UTF-8?q?=EB=B3=84=EB=8F=84=EC=9D=98=20API=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../sodalive/explorer/ExplorerController.kt   |  9 ++++++
 .../sodalive/explorer/ExplorerService.kt      | 28 +++++++++++++++++++
 2 files changed, 37 insertions(+)

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 1ce31f5..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<GetExplorerSectionResponse>()