Merge pull request '캔 쿠폰 시스템' (#107) from test into main
Reviewed-on: #107
This commit is contained in:
		| @@ -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") | ||||
| @@ -58,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") | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -111,6 +111,10 @@ class CanService(private val repository: CanRepository) { | ||||
|                         "제휴보상" | ||||
|                     } | ||||
|  | ||||
|                     ChargeStatus.COUPON -> { | ||||
|                         it.payment!!.method ?: "쿠폰충전" | ||||
|                     } | ||||
|  | ||||
|                     else -> { | ||||
|                         "환불" | ||||
|                     } | ||||
|   | ||||
| @@ -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,33 @@ 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("이미 사용한 쿠폰번호 입니다.") | ||||
|         } | ||||
|  | ||||
|         canCouponNumber.member = member | ||||
|  | ||||
|         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) | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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() | ||||
| @@ -0,0 +1,107 @@ | ||||
| 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 | ||||
| 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 | ||||
| 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 | ||||
| import java.net.URLEncoder | ||||
| import java.nio.charset.StandardCharsets | ||||
|  | ||||
| @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())) | ||||
|     } | ||||
|  | ||||
|     @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() | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     @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 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() | ||||
|             .headers(headers) | ||||
|             .contentType( | ||||
|                 MediaType | ||||
|                     .parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | ||||
|             ) | ||||
|             .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!! | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -0,0 +1,88 @@ | ||||
| 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<CanCouponNumber, Long>, CanCouponNumberQueryRepository | ||||
|  | ||||
| interface CanCouponNumberQueryRepository { | ||||
|     fun getUseCouponCount(id: Long): Int | ||||
|  | ||||
|     fun getCouponNumberTotalCount(couponId: Long): Int | ||||
|  | ||||
|     fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List<GetCouponNumberListItemResponse> | ||||
|  | ||||
|     fun getAllCouponNumberList(couponId: Long): List<GetCouponNumberListItemResponse> | ||||
|  | ||||
|     fun findByCouponNumber(couponNumber: String): CanCouponNumber? | ||||
|  | ||||
|     fun findByMemberId(memberId: Long): List<Long> | ||||
| } | ||||
|  | ||||
| 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 | ||||
|     } | ||||
|  | ||||
|     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<GetCouponNumberListItemResponse> { | ||||
|         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<GetCouponNumberListItemResponse> { | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetCouponNumberListItemResponse( | ||||
|                     canCouponNumber.id, | ||||
|                     canCouponNumber.couponNumber, | ||||
|                     canCouponNumber.member.isNotNull | ||||
|                 ) | ||||
|             ) | ||||
|             .from(canCouponNumber) | ||||
|             .where(canCouponNumber.canCoupon.id.eq(couponId)) | ||||
|             .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<Long> { | ||||
|         return queryFactory | ||||
|             .select(canCouponNumber.id) | ||||
|             .from(canCouponNumber) | ||||
|             .where(canCouponNumber.member.id.eq(memberId)) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| 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<CanCoupon, Long>, CanCouponQueryRepository | ||||
|  | ||||
| interface CanCouponQueryRepository { | ||||
|     fun getCouponTotalCount(): Int | ||||
|     fun getCouponList(offset: Long, limit: Long): List<CanCoupon> | ||||
| } | ||||
|  | ||||
| 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<CanCoupon> { | ||||
|         return queryFactory | ||||
|             .selectFrom(canCoupon) | ||||
|             .orderBy(canCoupon.id.desc()) | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,116 @@ | ||||
| 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 | ||||
| import java.io.IOException | ||||
| 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 | ||||
| ) { | ||||
|     fun generateCoupon(request: GenerateCanCouponRequest) { | ||||
|         val message = objectMapper.writeValueAsString(request) | ||||
|         applicationEventPublisher.publishEvent(SqsEvent(type = SqsEventType.GENERATE_COUPON, message = message)) | ||||
|     } | ||||
|  | ||||
|     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!!) | ||||
|                 GetCouponListItemResponse( | ||||
|                     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() | ||||
|  | ||||
|         return GetCouponListResponse(totalCount, items) | ||||
|     } | ||||
|  | ||||
|     fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): GetCouponNumberListResponse { | ||||
|         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 = couponNumberRepository.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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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("-") | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| package kr.co.vividnext.sodalive.can.coupon | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
|  | ||||
| data class GenerateCanCouponRequest( | ||||
|     @JsonProperty("couponName") val couponName: String, | ||||
|     @JsonProperty("can") val can: Int, | ||||
|     @JsonProperty("validity") val validity: String, | ||||
|     @JsonProperty("isMultipleUse") val isMultipleUse: Boolean, | ||||
|     @JsonProperty("couponNumberCount") val couponNumberCount: Int | ||||
| ) | ||||
| @@ -0,0 +1,17 @@ | ||||
| package kr.co.vividnext.sodalive.can.coupon | ||||
|  | ||||
| data class GetCouponListResponse( | ||||
|     val totalCount: Int, | ||||
|     val items: List<GetCouponListItemResponse> | ||||
| ) | ||||
|  | ||||
| data class GetCouponListItemResponse( | ||||
|     val id: Long, | ||||
|     val couponName: String, | ||||
|     val can: Int, | ||||
|     val couponCount: Int, | ||||
|     val useCouponCount: Int, | ||||
|     val validity: String, | ||||
|     val isMultipleUse: Boolean, | ||||
|     val isActive: Boolean | ||||
| ) | ||||
| @@ -0,0 +1,14 @@ | ||||
| package kr.co.vividnext.sodalive.can.coupon | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetCouponNumberListResponse( | ||||
|     val totalCount: Int, | ||||
|     val items: List<GetCouponNumberListItemResponse> | ||||
| ) | ||||
|  | ||||
| data class GetCouponNumberListItemResponse @QueryProjection constructor( | ||||
|     val couponNumberId: Long, | ||||
|     val couponNumber: String, | ||||
|     val isUsed: Boolean | ||||
| ) | ||||
| @@ -0,0 +1,3 @@ | ||||
| package kr.co.vividnext.sodalive.can.coupon | ||||
|  | ||||
| data class UseCanCouponRequest(val couponNumber: String) | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -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: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user