diff --git a/build.gradle.kts b/build.gradle.kts index 84666cf..38cc7a9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { kapt("org.springframework.boot:spring-boot-configuration-processor") // aws + implementation("com.amazonaws:aws-java-sdk-sqs:1.12.380") implementation("com.amazonaws:aws-java-sdk-ses:1.12.380") implementation("com.amazonaws:aws-java-sdk-s3:1.12.380") implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt new file mode 100644 index 0000000..f239665 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.aws.sqs + +import org.springframework.context.event.EventListener +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component + +enum class SqsEventType { + GENERATE_COUPON +} + +class SqsEvent( + val type: SqsEventType, + val message: String +) + +@Component +class SqsEventListener(private val sqsService: SqsService) { + @Async + @EventListener + fun sendMessage(sqsEvent: SqsEvent) { + when (sqsEvent.type) { + SqsEventType.GENERATE_COUPON -> sqsService.sendGenerateCouponMessage(sqsEvent.message) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsService.kt new file mode 100644 index 0000000..dd21e5d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsService.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.aws.sqs + +import com.amazonaws.services.sqs.AmazonSQS +import com.amazonaws.services.sqs.model.SendMessageRequest +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service + +@Service +class SqsService( + private val amazonSQS: AmazonSQS, + + @Value("\${cloud.aws.sqs.generate-coupon-url}") + private val generateCouponQueueUrl: String +) { + fun sendGenerateCouponMessage(message: String) { + val request = SendMessageRequest() + .withQueueUrl(generateCouponQueueUrl) + .withMessageBody(message) + + amazonSQS.sendMessage(request) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCoupon.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCoupon.kt new file mode 100644 index 0000000..d122cbb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCoupon.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.can.coupon + +import kr.co.vividnext.sodalive.common.BaseEntity +import java.time.LocalDateTime +import javax.persistence.Entity + +@Entity +data class CanCoupon( + val couponName: String, + val can: Int, + val couponCount: Int, + val validity: LocalDateTime, + val isActive: Boolean, + val isMultipleUse: Boolean +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt new file mode 100644 index 0000000..dd0eff5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt @@ -0,0 +1,39 @@ +package kr.co.vividnext.sodalive.can.coupon + +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.access.prepost.PreAuthorize +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.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/can/coupon") +class CanCouponController(private val service: CanCouponService) { + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun generateCoupon( + @RequestBody request: GenerateCanCouponRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.generateCoupon(request)) + } + + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + fun getCouponList( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getCouponList(offset = pageable.offset, limit = pageable.pageSize.toLong())) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumber.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumber.kt new file mode 100644 index 0000000..3238e54 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumber.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.can.coupon + +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 +import javax.persistence.OneToOne + +@Entity +data class CanCouponNumber( + val couponNumber: String +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "can_coupon_id", nullable = false) + var canCoupon: CanCoupon? = null + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = true) + var member: Member? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt new file mode 100644 index 0000000..b66268b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.can.coupon + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.coupon.QCanCouponNumber.canCouponNumber +import org.springframework.data.jpa.repository.JpaRepository + +interface CanCouponNumberRepository : JpaRepository, CanCouponNumberQueryRepository + +interface CanCouponNumberQueryRepository { + fun getUseCouponCount(id: Long): Int +} + +class CanCouponNumberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponNumberQueryRepository { + override fun getUseCouponCount(id: Long): Int { + return queryFactory + .select(canCouponNumber.id) + .from(canCouponNumber) + .where(canCouponNumber.member.isNotNull) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt new file mode 100644 index 0000000..bf7d7b7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.can.coupon + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.coupon.QCanCoupon.canCoupon +import org.springframework.data.jpa.repository.JpaRepository + +interface CanCouponRepository : JpaRepository, CanCouponQueryRepository + +interface CanCouponQueryRepository { + fun getCouponList(offset: Long, limit: Long): List +} + +class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository { + override fun getCouponList(offset: Long, limit: Long): List { + return queryFactory + .selectFrom(canCoupon) + .orderBy(canCoupon.id.desc()) + .offset(offset) + .limit(limit) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt new file mode 100644 index 0000000..7c38476 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.can.coupon + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.sqs.SqsEvent +import kr.co.vividnext.sodalive.aws.sqs.SqsEventType +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Service +import java.time.format.DateTimeFormatter + +@Service +class CanCouponService( + private val repository: CanCouponRepository, + private val couponNumberRepository: CanCouponNumberRepository, + + private val objectMapper: ObjectMapper, + private val applicationEventPublisher: ApplicationEventPublisher +) { + fun generateCoupon(request: GenerateCanCouponRequest) { + val message = objectMapper.writeValueAsString(request) + applicationEventPublisher.publishEvent(SqsEvent(type = SqsEventType.GENERATE_COUPON, message = message)) + } + + fun getCouponList(offset: Long, limit: Long): List { + return repository.getCouponList(offset = offset, limit = limit) + .asSequence() + .map { + val useCouponCount = couponNumberRepository.getUseCouponCount(id = it.id!!) + GetCouponListResponse( + id = it.id!!, + couponName = it.couponName, + can = "${it.can}캔", + couponCount = it.couponCount, + useCouponCount = useCouponCount, + validity = it.validity.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), + isMultipleUse = it.isMultipleUse, + isActive = it.isActive + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GenerateCanCouponRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GenerateCanCouponRequest.kt new file mode 100644 index 0000000..abdb895 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GenerateCanCouponRequest.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.can.coupon + +import com.fasterxml.jackson.annotation.JsonProperty +import java.time.LocalDateTime + +data class GenerateCanCouponRequest( + @JsonProperty("couponName") val couponName: String, + @JsonProperty("can") val can: Int, + @JsonProperty("validity") val validity: LocalDateTime, + @JsonProperty("isMultipleUse") val isMultipleUse: Boolean, + @JsonProperty("couponNumberCount") val couponNumberCount: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt new file mode 100644 index 0000000..3bec552 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.can.coupon + +data class GetCouponListResponse( + val id: Long, + val couponName: String, + val can: String, + val couponCount: Int, + val useCouponCount: Int, + val validity: String, + val isMultipleUse: Boolean, + val isActive: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonSQSConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonSQSConfig.kt new file mode 100644 index 0000000..ff7e7e1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonSQSConfig.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.configs + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.services.sqs.AmazonSQS +import com.amazonaws.services.sqs.AmazonSQSClientBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class AmazonSQSConfig( + @Value("\${cloud.aws.credentials.access-key}") + private val accessKey: String, + @Value("\${cloud.aws.credentials.secret-key}") + private val secretKey: String, + @Value("\${cloud.aws.region.static}") + private val region: String +) { + @Bean + fun amazonSQS(): AmazonSQS { + val basicAWSCredentials = BasicAWSCredentials(accessKey, secretKey) + val awsStaticCredentialsProvider = AWSStaticCredentialsProvider(basicAWSCredentials) + + return AmazonSQSClientBuilder.standard() + .withCredentials(awsStaticCredentialsProvider) + .withRegion(region) + .build() + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1f71979..62f24c6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -37,6 +37,8 @@ cloud: keyPairId: ${CONTENT_CLOUD_FRONT_KEY_PAIR_ID} cloudFront: host: ${CLOUD_FRONT_HOST} + sqs: + generateCouponUrl: ${AMAZON_SQS_GENERATE_CAN_COUPON_QUEUE_URL} region: static: ap-northeast-2 stack: