diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt new file mode 100644 index 0000000..bbb8fd0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.admin.can + +data class AdminCanChargeRequest( + val memberId: Long, + val method: String, + val can: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanController.kt new file mode 100644 index 0000000..042eb29 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanController.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +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("/admin/can") +@PreAuthorize("hasRole('ADMIN')") +class AdminCanController(private val service: AdminCanService) { + @PostMapping + fun insertCan(@RequestBody request: AdminCanRequest) = ApiResponse.ok(service.saveCan(request)) + + @DeleteMapping("/{id}") + fun deleteCan(@PathVariable id: Long) = ApiResponse.ok(service.deleteCan(id)) + + @PostMapping("/charge") + fun charge(@RequestBody request: AdminCanChargeRequest) = ApiResponse.ok(service.charge(request)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRepository.kt new file mode 100644 index 0000000..e784d13 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRepository.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.can.Can +import org.springframework.data.jpa.repository.JpaRepository + +interface AdminCanRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRequest.kt new file mode 100644 index 0000000..92258ff --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRequest.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.can.Can +import kr.co.vividnext.sodalive.can.CanStatus +import kr.co.vividnext.sodalive.extensions.moneyFormat + +data class AdminCanRequest( + val can: Int, + val rewardCan: Int, + val price: Int +) { + fun toEntity(): Can { + var title = "${can.moneyFormat()} 캔" + if (rewardCan > 0) { + title = "$title + ${rewardCan.moneyFormat()} 캔" + } + + return Can( + title = title, + can = can, + rewardCan = rewardCan, + price = price, + status = CanStatus.SALE + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt new file mode 100644 index 0000000..3cdd6e1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt @@ -0,0 +1,56 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.admin.member.AdminMemberRepository +import kr.co.vividnext.sodalive.can.CanStatus +import kr.co.vividnext.sodalive.can.charge.Charge +import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.payment.Payment +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.extensions.moneyFormat +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AdminCanService( + private val repository: AdminCanRepository, + private val chargeRepository: ChargeRepository, + private val memberRepository: AdminMemberRepository +) { + @Transactional + fun saveCan(request: AdminCanRequest) { + repository.save(request.toEntity()) + } + + @Transactional + fun deleteCan(id: Long) { + val can = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + can.status = CanStatus.END_OF_SALE + } + + @Transactional + fun charge(request: AdminCanChargeRequest) { + val member = memberRepository.findByIdOrNull(request.memberId) + ?: throw SodaException("잘못된 회원번호 입니다.") + + if (request.can <= 0) throw SodaException("0 코인 이상 입력하세요.") + if (request.method.isBlank()) throw SodaException("기록내용을 입력하세요.") + + val charge = Charge(0, request.can, status = ChargeStatus.ADMIN) + charge.title = "${request.can.moneyFormat()} 캔" + charge.member = member + + val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG) + payment.method = request.method + charge.payment = payment + + chargeRepository.save(charge) + + member.pgRewardCan += charge.rewardCan + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusController.kt new file mode 100644 index 0000000..be859be --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusController.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.admin.charge + +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@PreAuthorize("hasRole('ADMIN')") +@RequestMapping("/admin/charge/status") +class AdminChargeStatusController(private val service: AdminChargeStatusService) { + @GetMapping + fun getChargeStatus( + @RequestParam startDateStr: String, + @RequestParam endDateStr: String + ) = ApiResponse.ok(service.getChargeStatus(startDateStr, endDateStr)) + + @GetMapping("/detail") + fun getChargeStatusDetail( + @RequestParam startDateStr: String, + @RequestParam paymentGateway: PaymentGateway + ) = ApiResponse.ok(service.getChargeStatusDetail(startDateStr, paymentGateway)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusQueryRepository.kt new file mode 100644 index 0000000..cf2f704 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusQueryRepository.kt @@ -0,0 +1,90 @@ +package kr.co.vividnext.sodalive.admin.charge + +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.QCan.can1 +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.charge.QCharge.charge +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.can.payment.QPayment.payment +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class AdminChargeStatusQueryRepository(private val queryFactory: JPAQueryFactory) { + fun getChargeStatus(startDate: LocalDateTime, endDate: LocalDateTime): List { + val formattedDate = Expressions.stringTemplate( + "DATE_FORMAT({0}, {1})", + Expressions.dateTimeTemplate( + LocalDateTime::class.java, + "CONVERT_TZ({0},{1},{2})", + charge.createdAt, + "UTC", + "Asia/Seoul" + ), + "%Y-%m-%d" + ) + + return queryFactory + .select( + QGetChargeStatusQueryDto( + formattedDate, + payment.price.sum(), + can1.price.sum(), + payment.id.count(), + payment.paymentGateway + ) + ) + .from(payment) + .innerJoin(payment.charge, charge) + .leftJoin(charge.can, can1) + .where( + charge.createdAt.goe(startDate) + .and(charge.createdAt.loe(endDate)) + .and(charge.status.eq(ChargeStatus.CHARGE)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + ) + .groupBy(formattedDate, payment.paymentGateway) + .orderBy(formattedDate.desc()) + .fetch() + } + + fun getChargeStatusDetail(startDate: LocalDateTime, endDate: LocalDateTime): List { + val formattedDate = Expressions.stringTemplate( + "DATE_FORMAT({0}, {1})", + Expressions.dateTimeTemplate( + LocalDateTime::class.java, + "CONVERT_TZ({0},{1},{2})", + charge.createdAt, + "UTC", + "Asia/Seoul" + ), + "%Y-%m-%d %H:%i:%s" + ) + + return queryFactory + .select( + QGetChargeStatusDetailQueryDto( + member.id, + member.nickname, + payment.method.coalesce(""), + payment.price, + can1.price, + formattedDate + ) + ) + .from(charge) + .innerJoin(charge.member, member) + .innerJoin(charge.payment, payment) + .leftJoin(charge.can, can1) + .where( + charge.createdAt.goe(startDate) + .and(charge.createdAt.loe(endDate)) + .and(charge.status.eq(ChargeStatus.CHARGE)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + ) + .orderBy(formattedDate.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt new file mode 100644 index 0000000..1cb97c9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt @@ -0,0 +1,101 @@ +package kr.co.vividnext.sodalive.admin.charge + +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class AdminChargeStatusService(val repository: AdminChargeStatusQueryRepository) { + fun getChargeStatus(startDateStr: String, endDateStr: String): List { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val endDate = LocalDate.parse(endDateStr, dateTimeFormatter).atTime(23, 59, 59) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + var totalChargeAmount = 0 + var totalChargeCount = 0L + + val chargeStatusList = repository.getChargeStatus(startDate, endDate) + .asSequence() + .map { + val chargeAmount = if (it.paymentGateWay == PaymentGateway.APPLE_IAP) { + it.appleChargeAmount.toInt() + } else { + it.pgChargeAmount + } + + val chargeCount = it.chargeCount + + totalChargeAmount += chargeAmount + totalChargeCount += chargeCount + + GetChargeStatusResponse( + date = it.date, + chargeAmount = chargeAmount, + chargeCount = chargeCount, + pg = it.paymentGateWay.name + ) + } + .toMutableList() + + chargeStatusList.add( + 0, + GetChargeStatusResponse( + date = "합계", + chargeAmount = totalChargeAmount, + chargeCount = totalChargeCount, + pg = "" + ) + ) + + return chargeStatusList.toList() + } + + fun getChargeStatusDetail( + startDateStr: String, + paymentGateway: PaymentGateway + ): List { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val endDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(23, 59, 59) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + return repository.getChargeStatusDetail(startDate, endDate) + .asSequence() + .filter { + if (paymentGateway == PaymentGateway.APPLE_IAP) { + it.appleChargeAmount > 0 + } else { + it.pgChargeAmount > 0 + } + } + .map { + GetChargeStatusDetailResponse( + accountId = it.accountId, + nickname = it.nickname, + method = it.method, + amount = if (paymentGateway == PaymentGateway.APPLE_IAP) { + it.appleChargeAmount.toInt() + } else { + it.pgChargeAmount + }, + datetime = it.datetime + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt new file mode 100644 index 0000000..2d94030 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.admin.charge + +import com.querydsl.core.annotations.QueryProjection + +data class GetChargeStatusDetailQueryDto @QueryProjection constructor( + val accountId: Long, + val nickname: String, + val method: String, + val appleChargeAmount: Double, + val pgChargeAmount: Int, + val datetime: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt new file mode 100644 index 0000000..0d8ae2f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.admin.charge + +data class GetChargeStatusDetailResponse( + val accountId: Long, + val nickname: String, + val method: String, + val amount: Int, + val datetime: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusQueryDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusQueryDto.kt new file mode 100644 index 0000000..c5c16d0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusQueryDto.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.admin.charge + +import com.querydsl.core.annotations.QueryProjection +import kr.co.vividnext.sodalive.can.payment.PaymentGateway + +data class GetChargeStatusQueryDto @QueryProjection constructor( + val date: String, + val appleChargeAmount: Double, + val pgChargeAmount: Int, + val chargeCount: Long, + val paymentGateWay: PaymentGateway +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusResponse.kt new file mode 100644 index 0000000..efbe3aa --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.admin.charge + +data class GetChargeStatusResponse( + val date: String, + val chargeAmount: Int, + val chargeCount: Long, + val pg: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt index 78389da..d3de5c0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -10,9 +10,9 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/admin/member") +@PreAuthorize("hasRole('ADMIN')") class AdminMemberController(private val service: AdminMemberService) { @GetMapping("/list") - @PreAuthorize("hasRole('ADMIN')") fun getMemberList(pageable: Pageable) = ApiResponse.ok(service.getMemberList(pageable)) @GetMapping("/search") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt index d78139e..befa1e4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt @@ -14,20 +14,18 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/admin/member/tag") +@PreAuthorize("hasRole('ADMIN')") class AdminMemberTagController(private val service: AdminMemberTagService) { @PostMapping - @PreAuthorize("hasRole('ADMIN')") fun enrollmentCreatorTag( @RequestPart("image") image: MultipartFile, @RequestPart("request") requestString: String ) = ApiResponse.ok(service.uploadTagImage(image, requestString), "등록되었습니다.") @DeleteMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") fun deleteCreatorTag(@PathVariable id: Long) = ApiResponse.ok(service.deleteTag(id), "삭제되었습니다.") @PutMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") fun modifyCreatorTag( @PathVariable id: Long, @RequestPart("image") image: MultipartFile?, @@ -35,7 +33,6 @@ class AdminMemberTagController(private val service: AdminMemberTagService) { ) = ApiResponse.ok(service.modifyTag(id, image, requestString), "수정되었습니다.") @PutMapping("/orders") - @PreAuthorize("hasRole('ADMIN')") fun updateTagOrders( @RequestBody request: UpdateTagOrdersRequest ) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt new file mode 100644 index 0000000..3665094 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.extensions + +import java.text.DecimalFormat + +fun Int.moneyFormat(): String = DecimalFormat("###,###").format(this)