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, 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 }