From 38cf9e453d62555563b6b812712e09dd3aa04887 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 1 Jan 2024 04:46:57 +0900 Subject: [PATCH 01/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20-=20=EC=BF=A0=ED=8F=B0=20=EC=83=9D=EC=84=B1=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20=EC=BF=A0=ED=8F=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../co/vividnext/sodalive/aws/sqs/SqsEvent.kt | 25 +++++++++++ .../vividnext/sodalive/aws/sqs/SqsService.kt | 22 ++++++++++ .../sodalive/can/coupon/CanCoupon.kt | 15 +++++++ .../can/coupon/CanCouponController.kt | 39 ++++++++++++++++++ .../sodalive/can/coupon/CanCouponNumber.kt | 22 ++++++++++ .../can/coupon/CanCouponNumberRepository.kt | 22 ++++++++++ .../can/coupon/CanCouponRepository.kt | 22 ++++++++++ .../sodalive/can/coupon/CanCouponService.kt | 41 +++++++++++++++++++ .../can/coupon/GenerateCanCouponRequest.kt | 12 ++++++ .../can/coupon/GetCouponListResponse.kt | 12 ++++++ .../sodalive/configs/AmazonSQSConfig.kt | 30 ++++++++++++++ src/main/resources/application.yml | 2 + 13 files changed, 265 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCoupon.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumber.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GenerateCanCouponRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonSQSConfig.kt 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: From 209f1f4bd126b4badea969890e78e9a8397ff588 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 1 Jan 2024 06:22:03 +0900 Subject: [PATCH 02/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20-=20=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../can/coupon/CanCouponController.kt | 19 +++++++++++++++++++ .../can/coupon/CanCouponRepository.kt | 16 ++++++++++++++++ .../sodalive/can/coupon/CanCouponService.kt | 4 ++++ .../can/coupon/GetCouponNumberListResponse.kt | 9 +++++++++ 4 files changed, 48 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt 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 index dd0eff5..cfd7b79 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt @@ -10,6 +10,7 @@ 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.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -36,4 +37,22 @@ class CanCouponController(private val service: CanCouponService) { ApiResponse.ok(service.getCouponList(offset = pageable.offset, limit = pageable.pageSize.toLong())) } + + @GetMapping("/number-list") + @PreAuthorize("hasRole('ADMIN')") + fun getCouponNumberList( + @RequestParam couponId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.getCouponNumberList( + couponId = couponId, + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) + } } 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 index bf7d7b7..49a8b1e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt @@ -2,12 +2,14 @@ package kr.co.vividnext.sodalive.can.coupon import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.can.coupon.QCanCoupon.canCoupon +import kr.co.vividnext.sodalive.can.coupon.QCanCouponNumber.canCouponNumber import org.springframework.data.jpa.repository.JpaRepository interface CanCouponRepository : JpaRepository, CanCouponQueryRepository interface CanCouponQueryRepository { fun getCouponList(offset: Long, limit: Long): List + fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List } class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository { @@ -19,4 +21,18 @@ class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : .limit(limit) .fetch() } + + override fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { + return queryFactory + .select( + QGetCouponNumberListResponse( + canCouponNumber.id, + canCouponNumber.couponNumber, + canCouponNumber.member.isNotNull + ) + ) + .from(canCouponNumber) + .where(canCouponNumber.canCoupon.id.eq(couponId)) + .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 index 7c38476..679b5a7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -38,4 +38,8 @@ class CanCouponService( } .toList() } + + fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { + return repository.getCouponNumberList(couponId = couponId, offset = offset, limit = limit) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt new file mode 100644 index 0000000..a67d697 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.can.coupon + +import com.querydsl.core.annotations.QueryProjection + +data class GetCouponNumberListResponse @QueryProjection constructor( + val couponNumberId: Long, + val couponNumber: String, + val isUsed: Boolean +) From 3b97364f24080d28797cc5320cc8a753db48bece Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 2 Jan 2024 00:02:43 +0900 Subject: [PATCH 03/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20AS-IS:=20=EC=9D=91=EB=8B=B5=EA=B0=92?= =?UTF-8?q?=EC=97=90=20=EC=A0=84=EC=B2=B4=20=EA=B0=9C=EC=88=98=20=EC=97=86?= =?UTF-8?q?=EC=9D=8C=20TO-BE:=20=EC=9D=91=EB=8B=B5=EA=B0=92=EC=97=90=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EA=B0=9C=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/can/coupon/CanCouponRepository.kt | 9 +++++++++ .../vividnext/sodalive/can/coupon/CanCouponService.kt | 10 +++++++--- .../sodalive/can/coupon/GetCouponListResponse.kt | 5 +++++ 3 files changed, 21 insertions(+), 3 deletions(-) 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 index 49a8b1e..e84b387 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt @@ -8,11 +8,20 @@ import org.springframework.data.jpa.repository.JpaRepository interface CanCouponRepository : JpaRepository, CanCouponQueryRepository interface CanCouponQueryRepository { + fun getCouponTotalCount(): Int fun getCouponList(offset: Long, limit: Long): List fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List } class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository { + override fun getCouponTotalCount(): Int { + return queryFactory + .select(canCoupon.id) + .from(canCoupon) + .fetch() + .size + } + override fun getCouponList(offset: Long, limit: Long): List { return queryFactory .selectFrom(canCoupon) 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 index 679b5a7..20cb12f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -20,12 +20,14 @@ class CanCouponService( applicationEventPublisher.publishEvent(SqsEvent(type = SqsEventType.GENERATE_COUPON, message = message)) } - fun getCouponList(offset: Long, limit: Long): List { - return repository.getCouponList(offset = offset, limit = limit) + fun getCouponList(offset: Long, limit: Long): GetCouponListResponse { + val totalCount = repository.getCouponTotalCount() + + val items = repository.getCouponList(offset = offset, limit = limit) .asSequence() .map { val useCouponCount = couponNumberRepository.getUseCouponCount(id = it.id!!) - GetCouponListResponse( + GetCouponListItemResponse( id = it.id!!, couponName = it.couponName, can = "${it.can}캔", @@ -37,6 +39,8 @@ class CanCouponService( ) } .toList() + + return GetCouponListResponse(totalCount, items) } fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { 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 index 3bec552..72b5aaa 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt @@ -1,6 +1,11 @@ package kr.co.vividnext.sodalive.can.coupon data class GetCouponListResponse( + val totalCount: Int, + val items: List +) + +data class GetCouponListItemResponse( val id: Long, val couponName: String, val can: String, From ba2530ba550f8fe2ce5ce9809e3e0225bef5032f Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 2 Jan 2024 01:28:05 +0900 Subject: [PATCH 04/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20AS-IS:=20=EC=BA=94=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?String=20TO-BE:=20=EC=BA=94=20=ED=83=80=EC=9E=85=20Int?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt | 2 +- .../co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 20cb12f..8172521 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -30,7 +30,7 @@ class CanCouponService( GetCouponListItemResponse( id = it.id!!, couponName = it.couponName, - can = "${it.can}캔", + can = it.can, couponCount = it.couponCount, useCouponCount = useCouponCount, validity = it.validity.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), 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 index 72b5aaa..f26bab4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponListResponse.kt @@ -8,7 +8,7 @@ data class GetCouponListResponse( data class GetCouponListItemResponse( val id: Long, val couponName: String, - val can: String, + val can: Int, val couponCount: Int, val useCouponCount: Int, val validity: String, From f80b8248e86bf4bdb57cdc74c0417c1bf73cb261 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 2 Jan 2024 01:43:42 +0900 Subject: [PATCH 05/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20API=20=EC=88=98=EC=A0=95=20AS-IS:=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EA=B8=B0=EA=B0=84=20=ED=83=80=EC=9E=85=20LocalDateTime=20TO-BE?= =?UTF-8?q?:=20=EC=9C=A0=ED=9A=A8=EA=B8=B0=EA=B0=84=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20String?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/can/coupon/GenerateCanCouponRequest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index abdb895..98028b2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GenerateCanCouponRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GenerateCanCouponRequest.kt @@ -1,12 +1,11 @@ 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("validity") val validity: String, @JsonProperty("isMultipleUse") val isMultipleUse: Boolean, @JsonProperty("couponNumberCount") val couponNumberCount: Int ) From d20f51ceacba8735e093ef2408e9ed4af33d061b Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 2 Jan 2024 03:13:04 +0900 Subject: [PATCH 06/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API=20=EC=88=98=EC=A0=95=20-?= =?UTF-8?q?=20totalCount=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../can/coupon/CanCouponRepository.kt | 19 ++++++++++++++++--- .../sodalive/can/coupon/CanCouponService.kt | 6 ++++-- .../can/coupon/GetCouponNumberListResponse.kt | 7 ++++++- 3 files changed, 26 insertions(+), 6 deletions(-) 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 index e84b387..2c28256 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt @@ -10,7 +10,8 @@ interface CanCouponRepository : JpaRepository, CanCouponQueryRe interface CanCouponQueryRepository { fun getCouponTotalCount(): Int fun getCouponList(offset: Long, limit: Long): List - fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List + fun getCouponNumberTotalCount(couponId: Long): Int + fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List } class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository { @@ -31,10 +32,19 @@ class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : .fetch() } - override fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { + override fun getCouponNumberTotalCount(couponId: Long): Int { + return queryFactory + .select(canCouponNumber.id) + .from(canCouponNumber) + .where(canCouponNumber.canCoupon.id.eq(couponId)) + .fetch() + .size + } + + override fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { return queryFactory .select( - QGetCouponNumberListResponse( + QGetCouponNumberListItemResponse( canCouponNumber.id, canCouponNumber.couponNumber, canCouponNumber.member.isNotNull @@ -42,6 +52,9 @@ class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ) .from(canCouponNumber) .where(canCouponNumber.canCoupon.id.eq(couponId)) + .orderBy(canCouponNumber.id.asc()) + .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 index 8172521..5e219b7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -43,7 +43,9 @@ class CanCouponService( return GetCouponListResponse(totalCount, items) } - fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { - return repository.getCouponNumberList(couponId = couponId, offset = offset, limit = limit) + fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): GetCouponNumberListResponse { + val totalCount = repository.getCouponNumberTotalCount(couponId = couponId) + val items = repository.getCouponNumberList(couponId = couponId, offset = offset, limit = limit) + return GetCouponNumberListResponse(totalCount, items) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt index a67d697..a79c083 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/GetCouponNumberListResponse.kt @@ -2,7 +2,12 @@ package kr.co.vividnext.sodalive.can.coupon import com.querydsl.core.annotations.QueryProjection -data class GetCouponNumberListResponse @QueryProjection constructor( +data class GetCouponNumberListResponse( + val totalCount: Int, + val items: List +) + +data class GetCouponNumberListItemResponse @QueryProjection constructor( val couponNumberId: Long, val couponNumber: String, val isUsed: Boolean From f45e07c8793622ef709521a153c3cb45ed7682f9 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 2 Jan 2024 06:05:59 +0900 Subject: [PATCH 07/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20API=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 --- build.gradle.kts | 2 + .../can/coupon/CanCouponController.kt | 19 ++++++++ .../can/coupon/CanCouponRepository.kt | 17 +++++++ .../sodalive/can/coupon/CanCouponService.kt | 46 +++++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 38cc7a9..e34510a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,8 @@ dependencies { // firebase admin sdk implementation("com.google.firebase:firebase-admin:9.2.0") + implementation("org.apache.poi:poi-ooxml:5.2.3") + developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") 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 index cfd7b79..219996f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt @@ -4,6 +4,9 @@ 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.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping @@ -55,4 +58,20 @@ class CanCouponController(private val service: CanCouponService) { ) ) } + + @GetMapping("/number-list/download") + @PreAuthorize("hasRole('ADMIN')") + fun downloadCouponNumberList( + @RequestParam couponId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + val fileName = "쿠폰번호리스트.xlsx" + val response = service.downloadCouponNumberList(couponId) + ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attatchment;filename=$fileName") + .contentType(MediaType.parseMediaType("application/vnd.ms-excel")) + .body(response) + } } 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 index 2c28256..9c3653c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt @@ -12,6 +12,8 @@ interface CanCouponQueryRepository { fun getCouponList(offset: Long, limit: Long): List fun getCouponNumberTotalCount(couponId: Long): Int fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List + + fun getAllCouponNumberList(couponId: Long): List } class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository { @@ -57,4 +59,19 @@ class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : .limit(limit) .fetch() } + + override fun getAllCouponNumberList(couponId: Long): List { + return queryFactory + .select( + QGetCouponNumberListItemResponse( + canCouponNumber.id, + canCouponNumber.couponNumber, + canCouponNumber.member.isNotNull + ) + ) + .from(canCouponNumber) + .where(canCouponNumber.canCoupon.id.eq(couponId)) + .orderBy(canCouponNumber.id.asc()) + .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 index 5e219b7..9524e17 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -3,8 +3,13 @@ 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 kr.co.vividnext.sodalive.common.SodaException +import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException import java.time.format.DateTimeFormatter @Service @@ -48,4 +53,45 @@ class CanCouponService( val items = repository.getCouponNumberList(couponId = couponId, offset = offset, limit = limit) return GetCouponNumberListResponse(totalCount, items) } + + fun downloadCouponNumberList(couponId: Long): ByteArrayInputStream { + val header = listOf("순번", "쿠폰번호", "사용여부") + val byteArrayOutputStream = ByteArrayOutputStream() + val couponNumberList = repository.getAllCouponNumberList(couponId) + + val workbook = XSSFWorkbook() + try { + val sheet = workbook.createSheet() + val row = sheet.createRow(0) + header.forEachIndexed { index, string -> + val cell = row.createCell(index) + cell.setCellValue(string) + } + + couponNumberList.forEachIndexed { index, item -> + val couponNumberRow = sheet.createRow(index + 1) + couponNumberRow.createCell(0).setCellValue(item.couponNumberId.toString()) + couponNumberRow.createCell(1).setCellValue(insertHyphens(item.couponNumber)) + couponNumberRow.createCell(2).setCellValue( + if (item.isUsed) { + "O" + } else { + "X" + } + ) + } + + workbook.write(byteArrayOutputStream) + return ByteArrayInputStream(byteArrayOutputStream.toByteArray()) + } catch (e: IOException) { + throw SodaException("다운로드를 하지 못했습니다.\n다시 시도해 주세요.") + } finally { + workbook.close() + byteArrayOutputStream.close() + } + } + + private fun insertHyphens(input: String): String { + return input.chunked(4).joinToString("-") + } } From 123b21cab2f35c75d17d2396a234f40cb5364501 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 2 Jan 2024 06:27:58 +0900 Subject: [PATCH 08/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20API=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20MediaType=20openxmlformats=20=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../can/coupon/CanCouponController.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 index 219996f..c67c4f7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt @@ -3,6 +3,7 @@ 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.core.io.InputStreamResource import org.springframework.data.domain.Pageable import org.springframework.http.HttpHeaders import org.springframework.http.MediaType @@ -15,6 +16,8 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import java.net.URLEncoder +import java.nio.charset.StandardCharsets @RestController @RequestMapping("/can/coupon") @@ -68,10 +71,22 @@ class CanCouponController(private val service: CanCouponService) { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") val fileName = "쿠폰번호리스트.xlsx" + val encodedFileName = URLEncoder.encode( + fileName, + StandardCharsets.UTF_8.toString() + ).replace("+", "%20") + val contentDisposition = "attachment; filename*=UTF-8''$encodedFileName" + val headers = HttpHeaders().apply { + add(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) + } + val response = service.downloadCouponNumberList(couponId) ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attatchment;filename=$fileName") - .contentType(MediaType.parseMediaType("application/vnd.ms-excel")) - .body(response) + .headers(headers) + .contentType( + MediaType + .parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + ) + .body(InputStreamResource(response)) } } From fb66ea3347332862b9ff8eb0cb44fb1fe851dc0a Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 3 Jan 2024 03:21:30 +0900 Subject: [PATCH 09/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20-=20couponRepository=20->=20couponNumberRepository=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../can/coupon/CanCouponNumberRepository.kt | 47 +++++++++++++++++++ .../can/coupon/CanCouponRepository.kt | 46 ------------------ .../sodalive/can/coupon/CanCouponService.kt | 6 +-- 3 files changed, 50 insertions(+), 49 deletions(-) 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 index b66268b..c51c98c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt @@ -8,6 +8,12 @@ interface CanCouponNumberRepository : JpaRepository, CanC interface CanCouponNumberQueryRepository { fun getUseCouponCount(id: Long): Int + + fun getCouponNumberTotalCount(couponId: Long): Int + + fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List + + fun getAllCouponNumberList(couponId: Long): List } class CanCouponNumberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponNumberQueryRepository { @@ -19,4 +25,45 @@ class CanCouponNumberQueryRepositoryImpl(private val queryFactory: JPAQueryFacto .fetch() .size } + + override fun getCouponNumberTotalCount(couponId: Long): Int { + return queryFactory + .select(canCouponNumber.id) + .from(canCouponNumber) + .where(canCouponNumber.canCoupon.id.eq(couponId)) + .fetch() + .size + } + + override fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { + return queryFactory + .select( + QGetCouponNumberListItemResponse( + canCouponNumber.id, + canCouponNumber.couponNumber, + canCouponNumber.member.isNotNull + ) + ) + .from(canCouponNumber) + .where(canCouponNumber.canCoupon.id.eq(couponId)) + .orderBy(canCouponNumber.id.asc()) + .offset(offset) + .limit(limit) + .fetch() + } + + override fun getAllCouponNumberList(couponId: Long): List { + return queryFactory + .select( + QGetCouponNumberListItemResponse( + canCouponNumber.id, + canCouponNumber.couponNumber, + canCouponNumber.member.isNotNull + ) + ) + .from(canCouponNumber) + .where(canCouponNumber.canCoupon.id.eq(couponId)) + .orderBy(canCouponNumber.id.asc()) + .fetch() + } } 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 index 9c3653c..012ab52 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponRepository.kt @@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.can.coupon import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.can.coupon.QCanCoupon.canCoupon -import kr.co.vividnext.sodalive.can.coupon.QCanCouponNumber.canCouponNumber import org.springframework.data.jpa.repository.JpaRepository interface CanCouponRepository : JpaRepository, CanCouponQueryRepository @@ -10,10 +9,6 @@ interface CanCouponRepository : JpaRepository, CanCouponQueryRe interface CanCouponQueryRepository { fun getCouponTotalCount(): Int fun getCouponList(offset: Long, limit: Long): List - fun getCouponNumberTotalCount(couponId: Long): Int - fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List - - fun getAllCouponNumberList(couponId: Long): List } class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository { @@ -33,45 +28,4 @@ class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : .limit(limit) .fetch() } - - override fun getCouponNumberTotalCount(couponId: Long): Int { - return queryFactory - .select(canCouponNumber.id) - .from(canCouponNumber) - .where(canCouponNumber.canCoupon.id.eq(couponId)) - .fetch() - .size - } - - override fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List { - return queryFactory - .select( - QGetCouponNumberListItemResponse( - canCouponNumber.id, - canCouponNumber.couponNumber, - canCouponNumber.member.isNotNull - ) - ) - .from(canCouponNumber) - .where(canCouponNumber.canCoupon.id.eq(couponId)) - .orderBy(canCouponNumber.id.asc()) - .offset(offset) - .limit(limit) - .fetch() - } - - override fun getAllCouponNumberList(couponId: Long): List { - return queryFactory - .select( - QGetCouponNumberListItemResponse( - canCouponNumber.id, - canCouponNumber.couponNumber, - canCouponNumber.member.isNotNull - ) - ) - .from(canCouponNumber) - .where(canCouponNumber.canCoupon.id.eq(couponId)) - .orderBy(canCouponNumber.id.asc()) - .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 index 9524e17..e5b95bc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -49,15 +49,15 @@ class CanCouponService( } fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): GetCouponNumberListResponse { - val totalCount = repository.getCouponNumberTotalCount(couponId = couponId) - val items = repository.getCouponNumberList(couponId = couponId, offset = offset, limit = limit) + val totalCount = couponNumberRepository.getCouponNumberTotalCount(couponId = couponId) + val items = couponNumberRepository.getCouponNumberList(couponId = couponId, offset = offset, limit = limit) return GetCouponNumberListResponse(totalCount, items) } fun downloadCouponNumberList(couponId: Long): ByteArrayInputStream { val header = listOf("순번", "쿠폰번호", "사용여부") val byteArrayOutputStream = ByteArrayOutputStream() - val couponNumberList = repository.getAllCouponNumberList(couponId) + val couponNumberList = couponNumberRepository.getAllCouponNumberList(couponId) val workbook = XSSFWorkbook() try { From fbaa1aa14ce22a3d66ef6797cdb635d86fce8082 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 3 Jan 2024 04:11:32 +0900 Subject: [PATCH 10/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/can/charge/ChargeService.kt | 28 +++++++++++++++++ .../sodalive/can/charge/ChargeStatus.kt | 2 +- .../can/coupon/CanCouponController.kt | 15 +++++++++ .../can/coupon/CanCouponIssueService.kt | 31 +++++++++++++++++++ .../can/coupon/CanCouponNumberRepository.kt | 19 ++++++++++++ .../sodalive/can/coupon/CanCouponService.kt | 19 ++++++++++++ .../can/coupon/UseCanCouponRequest.kt | 3 ++ 7 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponIssueService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/UseCanCouponRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index a8756b5..25cb5ca 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import kr.co.bootpay.Bootpay import kr.co.vividnext.sodalive.can.CanRepository import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent +import kr.co.vividnext.sodalive.can.coupon.CanCouponNumberRepository import kr.co.vividnext.sodalive.can.payment.Payment import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.can.payment.PaymentStatus @@ -29,6 +30,8 @@ class ChargeService( private val chargeRepository: ChargeRepository, private val canRepository: CanRepository, private val memberRepository: MemberRepository, + private val couponNumberRepository: CanCouponNumberRepository, + private val objectMapper: ObjectMapper, private val okHttpClient: OkHttpClient, private val applicationEventPublisher: ApplicationEventPublisher, @@ -43,6 +46,31 @@ class ChargeService( private val appleInAppVerifyUrl: String ) { + @Transactional + fun chargeByCoupon(couponNumber: String, member: Member) { + val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber) + ?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.") + + if (canCouponNumber.member != null) { + throw SodaException("이미 사용한 쿠폰번호 입니다.") + } + + val coupon = canCouponNumber.canCoupon!! + val couponCharge = Charge(0, coupon.can, status = ChargeStatus.COUPON) + couponCharge.title = "${coupon.can} 캔" + couponCharge.member = member + + val payment = Payment( + status = PaymentStatus.COMPLETE, + paymentGateway = PaymentGateway.PG + ) + payment.method = coupon.couponName + couponCharge.payment = payment + chargeRepository.save(couponCharge) + + member.charge(0, coupon.can, "pg") + } + @Transactional fun charge(member: Member, request: ChargeRequest): ChargeResponse { val can = canRepository.findByIdOrNull(request.canId) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt index 1585dcd..c31a16d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt @@ -1,7 +1,7 @@ package kr.co.vividnext.sodalive.can.charge enum class ChargeStatus { - CHARGE, REFUND_CHARGE, EVENT, CANCEL, + CHARGE, REFUND_CHARGE, EVENT, COUPON, CANCEL, // 관리자 지급 ADMIN, 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 index c67c4f7..5962f3a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponController.kt @@ -89,4 +89,19 @@ class CanCouponController(private val service: CanCouponService) { ) .body(InputStreamResource(response)) } + + @PostMapping("/use") + fun useCanCoupon( + @RequestBody request: UseCanCouponRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.useCanCoupon( + couponNumber = request.couponNumber, + memberId = member.id!! + ) + ) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponIssueService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponIssueService.kt new file mode 100644 index 0000000..547e7fa --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponIssueService.kt @@ -0,0 +1,31 @@ +package kr.co.vividnext.sodalive.can.coupon + +import kr.co.vividnext.sodalive.common.SodaException +import org.springframework.stereotype.Service + +@Service +class CanCouponIssueService(private val couponNumberRepository: CanCouponNumberRepository) { + fun validateAvailableUseCoupon(couponNumber: String, memberId: Long) { + val canCouponNumber = checkCanCouponNumber(couponNumber) + + if (!isMultipleUse(canCouponNumber)) { + val canCouponNumberList = couponNumberRepository.findByMemberId(memberId = memberId) + if (canCouponNumberList.isNotEmpty()) { + throw SodaException("해당 쿠폰은 1회만 충전이 가능합니다.") + } + } + } + + private fun checkCanCouponNumber(couponNumber: String): CanCouponNumber { + val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber) + ?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.") + + if (canCouponNumber.member != null) { + throw SodaException("이미 사용한 쿠폰번호 입니다.") + } + + return canCouponNumber + } + + private fun isMultipleUse(canCouponNumber: CanCouponNumber) = canCouponNumber.canCoupon!!.isMultipleUse +} 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 index c51c98c..0e4ccd1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponNumberRepository.kt @@ -14,6 +14,10 @@ interface CanCouponNumberQueryRepository { fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List fun getAllCouponNumberList(couponId: Long): List + + fun findByCouponNumber(couponNumber: String): CanCouponNumber? + + fun findByMemberId(memberId: Long): List } class CanCouponNumberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponNumberQueryRepository { @@ -66,4 +70,19 @@ class CanCouponNumberQueryRepositoryImpl(private val queryFactory: JPAQueryFacto .orderBy(canCouponNumber.id.asc()) .fetch() } + + override fun findByCouponNumber(couponNumber: String): CanCouponNumber? { + return queryFactory + .selectFrom(canCouponNumber) + .where(canCouponNumber.couponNumber.eq(couponNumber)) + .fetchFirst() + } + + override fun findByMemberId(memberId: Long): List { + return queryFactory + .select(canCouponNumber.id) + .from(canCouponNumber) + .where(canCouponNumber.member.id.eq(memberId)) + .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 index e5b95bc..f0474b8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -3,9 +3,12 @@ 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 kr.co.vividnext.sodalive.can.charge.ChargeService import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.MemberRepository import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.context.ApplicationEventPublisher +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -14,9 +17,14 @@ import java.time.format.DateTimeFormatter @Service class CanCouponService( + private val issueService: CanCouponIssueService, + private val chargeService: ChargeService, + private val repository: CanCouponRepository, private val couponNumberRepository: CanCouponNumberRepository, + private val memberRepository: MemberRepository, + private val objectMapper: ObjectMapper, private val applicationEventPublisher: ApplicationEventPublisher ) { @@ -91,6 +99,17 @@ class CanCouponService( } } + fun useCanCoupon(couponNumber: String, memberId: Long) { + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (member.auth == null) throw SodaException("쿠폰은 본인인증을 하셔야 사용이 가능합니다.") + + issueService.validateAvailableUseCoupon(couponNumber, memberId) + + chargeService.chargeByCoupon(couponNumber, member) + } + private fun insertHyphens(input: String): String { return input.chunked(4).joinToString("-") } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/UseCanCouponRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/UseCanCouponRequest.kt new file mode 100644 index 0000000..b0083ea --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/UseCanCouponRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.can.coupon + +data class UseCanCouponRequest(val couponNumber: String) From 75f3d1dab3e80b8db0926ab8ea02fdcf3f547894 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 3 Jan 2024 05:25:36 +0900 Subject: [PATCH 11/12] =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20API=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=EC=BF=A0=ED=8F=B0=20=EC=82=AC=EC=9A=A9=EB=82=B4=EC=97=AD?= =?UTF-8?q?=EC=9D=84=20=EA=B8=B0=EB=A1=9D=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index 25cb5ca..d711cb5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -55,6 +55,8 @@ class ChargeService( throw SodaException("이미 사용한 쿠폰번호 입니다.") } + canCouponNumber.member = member + val coupon = canCouponNumber.canCoupon!! val couponCharge = Charge(0, coupon.can, status = ChargeStatus.COUPON) couponCharge.title = "${coupon.can} 캔" From 538d4288bba0a047f013fe976fbf8d91edab75a1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 3 Jan 2024 05:27:44 +0900 Subject: [PATCH 12/12] =?UTF-8?q?=EC=B6=A9=EC=A0=84=EB=82=B4=EC=97=AD=20-?= =?UTF-8?q?=20=EC=BF=A0=ED=8F=B0=EC=9C=BC=EB=A1=9C=20=EC=B6=A9=EC=A0=84?= =?UTF-8?q?=EC=8B=9C=20=EC=BF=A0=ED=8F=B0=EC=9D=B4=EB=A6=84=20=ED=98=B9?= =?UTF-8?q?=EC=9D=80=20'=EC=BF=A0=ED=8F=B0=EC=B6=A9=EC=A0=84'=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= 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 | 4 ++++ 1 file changed, 4 insertions(+) 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 1c0ca4b..182ca9d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt @@ -111,6 +111,10 @@ class CanService(private val repository: CanRepository) { "제휴보상" } + ChargeStatus.COUPON -> { + it.payment!!.method ?: "쿠폰충전" + } + else -> { "환불" }