Compare commits

...

3 Commits

Author SHA1 Message Date
Klaus cd0c066978 앱 - 오디션 투표 API 2025-01-03 00:09:53 +09:00
Klaus 7a395a9906 앱 - 오디션 지원 API
- 기존에 지원한 내역이 있으면 false 처리 후 지원
2025-01-02 22:46:57 +09:00
Klaus 96f571e0c4 앱 - 오디션 지원 API 2025-01-02 19:32:31 +09:00
10 changed files with 247 additions and 4 deletions

View File

@ -9,9 +9,9 @@ import javax.persistence.ManyToOne
@Entity
data class AuditionApplicant(
val voicePath: String,
val phoneNumber: String,
val isActive: Boolean = true
var voicePath: String? = null,
var isActive: Boolean = true
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", nullable = false)

View File

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

View File

@ -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
)
)
}
}

View File

@ -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()
}
}

View File

@ -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,41 @@ 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 existingApplicant = repository.findActiveApplicantByMemberIdAndRoleId(
memberId = member.id!!,
roleId = auditionRole.id!!
)
if (existingApplicant != null) {
existingApplicant.isActive = false
repository.save(existingApplicant)
}
val applicant = AuditionApplicant(phoneNumber = request.phoneNumber)
applicant.role = auditionRole
applicant.member = member
repository.save(applicant)
val contentFileName = generateFileName(prefix = "${applicant.id}-applicant")
val metadata = ObjectMetadata()
metadata.contentLength = contentFile.size
val contentPath = s3Uploader.upload(
inputStream = contentFile.inputStream,
bucket = bucket,
filePath = "audition/${applicant.id}/$contentFileName",
metadata = metadata
)
applicant.voicePath = contentPath
}
}

View File

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

View File

@ -0,0 +1,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
}
}

View File

@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.audition.vote
import kr.co.vividnext.sodalive.audition.AuditionVote
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantRepository
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.ZonedDateTime
@Service
class AuditionVoteService(
private val repository: AuditionVoteRepository,
private val applicantRepository: AuditionApplicantRepository,
private val canPaymentService: CanPaymentService
) {
fun voteAuditionApplicant(applicantId: Long, timezone: String, container: String, member: Member) {
val applicant = applicantRepository.findByIdOrNull(applicantId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
val defaultZoneId = ZoneId.of("Asia/Seoul")
val clientZoneId = try {
ZoneId.of(timezone)
} catch (e: Exception) {
defaultZoneId
}
val nowInClientZone = ZonedDateTime.now(clientZoneId)
val startOfDayClient = nowInClientZone.toLocalDate().atStartOfDay(clientZoneId)
val endOfDayClient = startOfDayClient.plusDays(1).minusSeconds(1)
val startDate = startOfDayClient.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
val endDate = endOfDayClient.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
val voteCount = repository.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)
}
}

View File

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

View File

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