Merge pull request 'payverse 적용' (#344) from test into main

Reviewed-on: #344
This commit is contained in:
2025-10-10 07:44:07 +00:00
38 changed files with 704 additions and 122 deletions

View File

@@ -39,7 +39,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
@@ -75,7 +78,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
@@ -142,7 +148,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(order.isActive.isTrue)
.groupBy(
member.id,
@@ -230,7 +239,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
@@ -251,7 +263,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
@@ -281,7 +296,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
@@ -301,7 +319,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
@@ -331,7 +352,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
@@ -351,7 +375,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
@@ -382,7 +409,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.admin.calculate.ratio
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.Member
import java.time.LocalDateTime
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
@@ -9,12 +10,29 @@ import javax.persistence.OneToOne
@Entity
data class CreatorSettlementRatio(
val subsidy: Int,
val liveSettlementRatio: Int,
val contentSettlementRatio: Int,
val communitySettlementRatio: Int
var subsidy: Int,
var liveSettlementRatio: Int,
var contentSettlementRatio: Int,
var communitySettlementRatio: Int
) : BaseEntity() {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member? = null
var deletedAt: LocalDateTime? = null
fun softDelete() {
this.deletedAt = LocalDateTime.now()
}
fun restore() {
this.deletedAt = null
}
fun updateValues(subsidy: Int, live: Int, content: Int, community: Int) {
this.subsidy = subsidy
this.liveSettlementRatio = live
this.contentSettlementRatio = content
this.communitySettlementRatio = community
}
}

View File

@@ -4,6 +4,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
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
@@ -27,4 +28,14 @@ class CreatorSettlementRatioController(private val service: CreatorSettlementRat
limit = pageable.pageSize.toLong()
)
)
@PostMapping("/update")
fun updateCreatorSettlementRatio(
@RequestBody request: CreateCreatorSettlementRatioRequest
) = ApiResponse.ok(service.updateCreatorSettlementRatio(request))
@PostMapping("/delete/{memberId}")
fun deleteCreatorSettlementRatio(
@PathVariable memberId: Long
) = ApiResponse.ok(service.deleteCreatorSettlementRatio(memberId))
}

View File

@@ -7,7 +7,9 @@ import org.springframework.data.jpa.repository.JpaRepository
interface CreatorSettlementRatioRepository :
JpaRepository<CreatorSettlementRatio, Long>,
CreatorSettlementRatioQueryRepository
CreatorSettlementRatioQueryRepository {
fun findByMemberId(memberId: Long): CreatorSettlementRatio?
}
interface CreatorSettlementRatioQueryRepository {
fun getCreatorSettlementRatio(offset: Long, limit: Long): List<GetCreatorSettlementRatioItem>
@@ -21,6 +23,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
return queryFactory
.select(
QGetCreatorSettlementRatioItem(
member.id,
member.nickname,
creatorSettlementRatio.subsidy,
creatorSettlementRatio.liveSettlementRatio,
@@ -30,6 +33,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
)
.from(creatorSettlementRatio)
.innerJoin(creatorSettlementRatio.member, member)
.where(creatorSettlementRatio.deletedAt.isNull)
.orderBy(creatorSettlementRatio.id.asc())
.offset(offset)
.limit(limit)
@@ -40,6 +44,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
return queryFactory
.select(creatorSettlementRatio.id)
.from(creatorSettlementRatio)
.where(creatorSettlementRatio.deletedAt.isNull)
.fetch()
.size
}

View File

@@ -14,8 +14,6 @@ class CreatorSettlementRatioService(
) {
@Transactional
fun createCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
val creatorSettlementRatio = request.toEntity()
val creator = memberRepository.findByIdOrNull(request.memberId)
?: throw SodaException("잘못된 크리에이터 입니다.")
@@ -23,10 +21,52 @@ class CreatorSettlementRatioService(
throw SodaException("잘못된 크리에이터 입니다.")
}
val existing = repository.findByMemberId(request.memberId)
if (existing != null) {
// revive if soft-deleted, then update values
existing.restore()
existing.updateValues(
request.subsidy,
request.liveSettlementRatio,
request.contentSettlementRatio,
request.communitySettlementRatio
)
repository.save(existing)
return
}
val creatorSettlementRatio = request.toEntity()
creatorSettlementRatio.member = creator
repository.save(creatorSettlementRatio)
}
@Transactional
fun updateCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
val creator = memberRepository.findByIdOrNull(request.memberId)
?: throw SodaException("잘못된 크리에이터 입니다.")
if (creator.role != MemberRole.CREATOR) {
throw SodaException("잘못된 크리에이터 입니다.")
}
val existing = repository.findByMemberId(request.memberId)
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.")
existing.restore()
existing.updateValues(
request.subsidy,
request.liveSettlementRatio,
request.contentSettlementRatio,
request.communitySettlementRatio
)
repository.save(existing)
}
@Transactional
fun deleteCreatorSettlementRatio(memberId: Long) {
val existing = repository.findByMemberId(memberId)
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.")
existing.softDelete()
repository.save(existing)
}
@Transactional(readOnly = true)
fun getCreatorSettlementRatio(offset: Long, limit: Long): GetCreatorSettlementRatioResponse {
val totalCount = repository.getCreatorSettlementRatioTotalCount()

View File

@@ -8,6 +8,7 @@ data class GetCreatorSettlementRatioResponse(
)
data class GetCreatorSettlementRatioItem @QueryProjection constructor(
val memberId: Long,
val nickname: String,
val subsidy: Int,
val liveSettlementRatio: Int,

View File

@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.can.CanResponse
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.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
@@ -13,6 +15,11 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/admin/can")
@PreAuthorize("hasRole('ADMIN')")
class AdminCanController(private val service: AdminCanService) {
@GetMapping
fun getCans(): ApiResponse<List<CanResponse>> {
return ApiResponse.ok(service.getCans())
}
@PostMapping
fun insertCan(@RequestBody request: AdminCanRequest) = ApiResponse.ok(service.saveCan(request))

View File

@@ -1,6 +1,38 @@
package kr.co.vividnext.sodalive.admin.can
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.Can
import kr.co.vividnext.sodalive.can.CanResponse
import kr.co.vividnext.sodalive.can.CanStatus
import kr.co.vividnext.sodalive.can.QCan.can1
import kr.co.vividnext.sodalive.can.QCanResponse
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
interface AdminCanRepository : JpaRepository<Can, Long>
interface AdminCanRepository : JpaRepository<Can, Long>, AdminCanQueryRepository
interface AdminCanQueryRepository {
fun findAllByStatus(status: CanStatus): List<CanResponse>
}
@Repository
class AdminCanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminCanQueryRepository {
override fun findAllByStatus(status: CanStatus): List<CanResponse> {
return queryFactory
.select(
QCanResponse(
can1.id,
can1.title,
can1.can,
can1.rewardCan,
can1.price.intValue(),
can1.currency,
can1.price.stringValue()
)
)
.from(can1)
.where(can1.status.eq(status))
.orderBy(can1.currency.asc(), can1.price.asc())
.fetch()
}
}

View File

@@ -3,11 +3,13 @@ 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
import java.math.BigDecimal
data class AdminCanRequest(
val can: Int,
val rewardCan: Int,
val price: Int
val price: BigDecimal,
val currency: String
) {
fun toEntity(): Can {
var title = "${can.moneyFormat()}"
@@ -20,6 +22,7 @@ data class AdminCanRequest(
can = can,
rewardCan = rewardCan,
price = price,
currency = currency,
status = CanStatus.SALE
)
}

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.admin.member.AdminMemberRepository
import kr.co.vividnext.sodalive.can.CanResponse
import kr.co.vividnext.sodalive.can.CanStatus
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
@@ -20,6 +21,10 @@ class AdminCanService(
private val chargeRepository: ChargeRepository,
private val memberRepository: AdminMemberRepository
) {
fun getCans(): List<CanResponse> {
return repository.findAllByStatus(status = CanStatus.SALE)
}
@Transactional
fun saveCan(request: AdminCanRequest) {
repository.save(request.toEntity())

View File

@@ -20,16 +20,15 @@ class AdminChargeStatusService(val repository: AdminChargeStatusQueryRepository)
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
var totalChargeAmount = 0
var totalChargeAmount = 0.toBigDecimal()
var totalChargeCount = 0L
val chargeStatusList = repository.getChargeStatus(startDate, endDate)
.asSequence()
.map {
val chargeAmount = if (it.paymentGateWay == PaymentGateway.PG) {
it.pgChargeAmount
} else {
it.appleChargeAmount.toInt()
it.appleChargeAmount
}
val chargeCount = it.chargeCount

View File

@@ -1,13 +1,14 @@
package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.annotations.QueryProjection
import java.math.BigDecimal
data class GetChargeStatusDetailQueryDto @QueryProjection constructor(
val memberId: Long,
val nickname: String,
val method: String,
val appleChargeAmount: Double,
val pgChargeAmount: Int,
val appleChargeAmount: BigDecimal,
val pgChargeAmount: BigDecimal,
val locale: String,
val datetime: String
)

View File

@@ -2,11 +2,12 @@ package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import java.math.BigDecimal
data class GetChargeStatusQueryDto @QueryProjection constructor(
val date: String,
val appleChargeAmount: Double,
val pgChargeAmount: Int,
val appleChargeAmount: BigDecimal,
val pgChargeAmount: BigDecimal,
val chargeCount: Long,
val paymentGateWay: PaymentGateway
)

View File

@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.admin.charge
import java.math.BigDecimal
data class GetChargeStatusResponse(
val date: String,
val chargeAmount: Int,
val chargeAmount: BigDecimal,
val chargeCount: Long,
val pg: String
)

View File

@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.admin.statistics.ad
import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.core.types.dsl.DateTimePath
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.core.types.dsl.NumberExpression
import com.querydsl.core.types.dsl.StringTemplate
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
@@ -67,7 +66,7 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
val firstPaymentTotalAmount = CaseBuilder()
.`when`(adTrackingHistory.type.eq(AdTrackingHistoryType.FIRST_PAYMENT))
.then(adTrackingHistory.price)
.otherwise(Expressions.constant(0.0))
.otherwise(0.toBigDecimal())
.sum()
val repeatPaymentCount = CaseBuilder()
@@ -79,7 +78,7 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
val repeatPaymentTotalAmount = CaseBuilder()
.`when`(adTrackingHistory.type.eq(AdTrackingHistoryType.REPEAT_PAYMENT))
.then(adTrackingHistory.price)
.otherwise(Expressions.constant(0.0))
.otherwise(0.toBigDecimal())
.sum()
val allPaymentCount = CaseBuilder()
@@ -97,7 +96,7 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
.or(adTrackingHistory.type.eq(AdTrackingHistoryType.REPEAT_PAYMENT))
)
.then(adTrackingHistory.price)
.otherwise(Expressions.constant(0.0))
.otherwise(0.toBigDecimal())
.sum()
return queryFactory
@@ -111,11 +110,11 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
loginCount,
signUpCount,
firstPaymentCount,
roundedValueDecimalPlaces2(firstPaymentTotalAmount),
firstPaymentTotalAmount,
repeatPaymentCount,
roundedValueDecimalPlaces2(repeatPaymentTotalAmount),
repeatPaymentTotalAmount,
allPaymentCount,
roundedValueDecimalPlaces2(allPaymentTotalAmount)
allPaymentTotalAmount
)
)
.from(adTrackingHistory)
@@ -148,13 +147,4 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
"%Y-%m-%d"
)
}
private fun roundedValueDecimalPlaces2(valueExpression: NumberExpression<Double>): NumberExpression<Double> {
return Expressions.numberTemplate(
Double::class.java,
"ROUND({0}, {1})",
valueExpression,
2
)
}
}

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.admin.statistics.ad
import com.querydsl.core.annotations.QueryProjection
import java.math.BigDecimal
data class GetAdminAdStatisticsResponse(
val totalCount: Int,
@@ -16,9 +17,9 @@ data class GetAdminAdStatisticsItem @QueryProjection constructor(
val loginCount: Int,
val signUpCount: Int,
val firstPaymentCount: Int,
val firstPaymentTotalAmount: Double,
val firstPaymentTotalAmount: BigDecimal,
val repeatPaymentCount: Int,
val repeatPaymentTotalAmount: Double,
val repeatPaymentTotalAmount: BigDecimal,
val allPaymentCount: Int,
val allPaymentTotalAmount: Double
val allPaymentTotalAmount: BigDecimal
)

View File

@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.can
import kr.co.vividnext.sodalive.common.BaseEntity
import java.math.BigDecimal
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
@@ -10,7 +12,10 @@ data class Can(
var title: String,
var can: Int,
var rewardCan: Int,
var price: Int,
@Column(precision = 10, scale = 4, nullable = false)
var price: BigDecimal,
@Column(length = 3, nullable = false, columnDefinition = "CHAR(3)")
var currency: String,
@Enumerated(value = EnumType.STRING)
var status: CanStatus
) : BaseEntity()

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.can
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.GeoCountry
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable
@@ -9,13 +10,15 @@ 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
import javax.servlet.http.HttpServletRequest
@RestController
@RequestMapping("/can")
class CanController(private val service: CanService) {
@GetMapping
fun getCans(): ApiResponse<List<CanResponse>> {
return ApiResponse.ok(service.getCans())
fun getCans(request: HttpServletRequest): ApiResponse<List<CanResponse>> {
val geoCountry = request.getAttribute("geoCountry") as? GeoCountry ?: GeoCountry.OTHER
return ApiResponse.ok(service.getCans(geoCountry))
}
@GetMapping("/status")

View File

@@ -23,7 +23,7 @@ import org.springframework.stereotype.Repository
interface CanRepository : JpaRepository<Can, Long>, CanQueryRepository
interface CanQueryRepository {
fun findAllByStatus(status: CanStatus): List<CanResponse>
fun findAllByStatusAndCurrency(status: CanStatus, currency: String): List<CanResponse>
fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan>
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
@@ -32,7 +32,7 @@ interface CanQueryRepository {
@Repository
class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQueryRepository {
override fun findAllByStatus(status: CanStatus): List<CanResponse> {
override fun findAllByStatusAndCurrency(status: CanStatus, currency: String): List<CanResponse> {
return queryFactory
.select(
QCanResponse(
@@ -40,11 +40,16 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
can1.title,
can1.can,
can1.rewardCan,
can1.price
can1.price.intValue(),
can1.currency,
can1.price.stringValue()
)
)
.from(can1)
.where(can1.status.eq(status))
.where(
can1.status.eq(status),
can1.currency.eq(currency)
)
.orderBy(can1.can.asc())
.fetch()
}
@@ -64,11 +69,13 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
val chargeStatusCondition = when (container) {
"aos" -> {
charge.payment.paymentGateway.eq(PaymentGateway.PG)
.or(charge.payment.paymentGateway.eq(PaymentGateway.PAYVERSE))
.or(charge.payment.paymentGateway.eq(PaymentGateway.GOOGLE_IAP))
}
"ios" -> {
charge.payment.paymentGateway.eq(PaymentGateway.PG)
.or(charge.payment.paymentGateway.eq(PaymentGateway.PAYVERSE))
.or(charge.payment.paymentGateway.eq(PaymentGateway.APPLE_IAP))
}

View File

@@ -7,5 +7,7 @@ data class CanResponse @QueryProjection constructor(
val title: String,
val can: Int,
val rewardCan: Int,
val price: Int
val price: Int,
val currency: String,
val priceStr: String
)

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.can
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.GeoCountry
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
@@ -11,8 +12,12 @@ import java.time.format.DateTimeFormatter
@Service
class CanService(private val repository: CanRepository) {
fun getCans(): List<CanResponse> {
return repository.findAllByStatus(status = CanStatus.SALE)
fun getCans(geoCountry: GeoCountry): List<CanResponse> {
val currency = when (geoCountry) {
GeoCountry.KR -> "KRW"
else -> "USD"
}
return repository.findAllByStatusAndCurrency(status = CanStatus.SALE, currency = currency)
}
fun getCanStatus(member: Member, container: String): GetCanStatusResponse {

View File

@@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.can.charge
import java.math.BigDecimal
data class ChargeCompleteResponse(
val price: Double,
val price: BigDecimal,
val currencyCode: String,
val isFirstCharged: Boolean
)

View File

@@ -6,20 +6,77 @@ import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
import kr.co.vividnext.sodalive.marketing.AdTrackingService
import kr.co.vividnext.sodalive.member.Member
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest
@RestController
@RequestMapping("/charge")
class ChargeController(
private val service: ChargeService,
private val trackingService: AdTrackingService
private val trackingService: AdTrackingService,
@Value("\${payverse.inbound-ip}")
private val payverseInboundIp: String
) {
@PostMapping("/payverse")
fun payverseCharge(
@RequestBody request: PayverseChargeRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
}
ApiResponse.ok(service.payverseCharge(member, request))
}
@PostMapping("/payverse/verify")
fun payverseVerify(
@RequestBody verifyRequest: PayverseVerifyRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
}
val response = service.payverseVerify(memberId = member.id!!, verifyRequest)
trackingCharge(member, response)
ApiResponse.ok(Unit)
}
// Payverse Webhook 엔드포인트 (payverseVerify 아래)
@PostMapping("/payverse/webhook")
fun payverseWebhook(
@RequestBody request: PayverseWebhookRequest,
servletRequest: HttpServletRequest
): PayverseWebhookResponse {
val header = servletRequest.getHeader("X-Forwarded-For")
val remoteIp = if (header.isNullOrEmpty()) {
servletRequest.remoteAddr
} else {
header.split(",")[0].trim() // 첫 번째 값이 클라이언트 IP
}
if (remoteIp != payverseInboundIp) {
throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
val success = service.payverseWebhook(request)
if (!success) {
throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
return PayverseWebhookResponse(receiveResult = "SUCCESS")
}
@PostMapping
fun charge(
@RequestBody chargeRequest: ChargeRequest,
@@ -111,8 +168,7 @@ class ChargeController(
memberId = member.id!!,
chargeId = chargeId,
productId = request.productId,
purchaseToken = request.purchaseToken,
paymentGateway = request.paymentGateway
purchaseToken = request.purchaseToken
)
trackingCharge(member, response)

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.can.charge
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import java.math.BigDecimal
data class ChargeRequest(val canId: Long, val paymentGateway: PaymentGateway)
@@ -20,14 +21,14 @@ data class VerifyResult(
val method: String,
val pg: String,
val status: Int,
val price: Int
val price: BigDecimal
)
data class AppleChargeRequest(
val title: String,
val chargeCan: Int,
val paymentGateway: PaymentGateway,
var price: Double? = null,
var price: BigDecimal? = null,
var locale: String? = null
)
@@ -38,9 +39,53 @@ data class AppleVerifyResponse(val status: Int)
data class GoogleChargeRequest(
val title: String,
val chargeCan: Int,
val price: Double,
val price: BigDecimal,
val currencyCode: String,
val productId: String,
val purchaseToken: String,
val paymentGateway: PaymentGateway
)
data class PayverseChargeRequest(
val canId: Long
)
data class PayverseChargeResponse(
val chargeId: Long,
val payloadJson: String
)
data class PayverseVerifyRequest(
val transactionId: String,
val orderId: String
)
data class PayverseVerifyResponse(
val resultStatus: String,
val tid: String,
val schemeGroup: String,
val schemeCode: String,
val transactionType: String,
val transactionStatus: String,
val transactionMessage: String,
val orderId: String,
val customerId: String,
val requestCurrency: String,
val requestAmount: BigDecimal
)
data class PayverseWebhookRequest(
val type: String,
val mid: String,
val tid: String,
val schemeGroup: String,
val schemeCode: String,
val orderId: String,
val requestCurrency: String,
val requestAmount: BigDecimal,
val resultStatus: String,
val approvalDay: String,
val sign: String
)
data class PayverseWebhookResponse(val receiveResult: String)

View File

@@ -22,6 +22,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.apache.commons.codec.digest.DigestUtils
import org.json.JSONObject
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
@@ -34,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Service
@Transactional(readOnly = true)
@@ -63,9 +65,112 @@ class ChargeService(
@Value("\${apple.iap-verify-sandbox-url}")
private val appleInAppVerifySandBoxUrl: String,
@Value("\${apple.iap-verify-url}")
private val appleInAppVerifyUrl: String
private val appleInAppVerifyUrl: String,
@Value("\${payverse.mid}")
private val payverseMid: String,
@Value("\${payverse.client-key}")
private val payverseClientKey: String,
@Value("\${payverse.secret-key}")
private val payverseSecretKey: String,
@Value("\${payverse.usd-mid}")
private val payverseUsdMid: String,
@Value("\${payverse.usd-client-key}")
private val payverseUsdClientKey: String,
@Value("\${payverse.usd-secret-key}")
private val payverseUsdSecretKey: String,
@Value("\${payverse.host}")
private val payverseHost: String,
@Value("\${server.env}")
private val serverEnv: String
) {
@Transactional
fun payverseWebhook(request: PayverseWebhookRequest): Boolean {
val chargeId = request.orderId.toLongOrNull() ?: return false
val charge = chargeRepository.findByIdOrNull(chargeId) ?: return false
// 결제수단 확인
if (charge.payment?.paymentGateway != PaymentGateway.PAYVERSE) {
return false
}
// 결제 상태 분기 처리
return when (charge.payment?.status) {
PaymentStatus.REQUEST -> {
// 성공 조건 검증
val mid = if (request.requestCurrency == "KRW") {
payverseMid
} else {
payverseUsdMid
}
val expectedSign = DigestUtils.sha512Hex(
String.format(
"||%s||%s||%s||%s||%s||",
if (request.requestCurrency == "KRW") {
payverseSecretKey
} else {
payverseUsdSecretKey
},
mid,
request.orderId,
request.requestAmount,
request.approvalDay
)
)
val isAmountMatch = request.requestAmount.compareTo(
charge.payment!!.price
) == 0
val isSuccess = request.resultStatus == "SUCCESS" &&
request.mid == mid &&
request.orderId.toLongOrNull() == charge.id &&
isAmountMatch &&
request.sign == expectedSign
if (isSuccess) {
// payverseVerify의 226~246 라인과 동일 처리
charge.payment?.receiptId = request.tid
val mappedMethod = if (request.schemeGroup == "PVKR") {
mapPayverseSchemeToMethodByCode(request.schemeCode)
} else {
null
}
charge.payment?.method = mappedMethod ?: request.schemeCode
charge.payment?.status = PaymentStatus.COMPLETE
charge.payment?.locale = request.requestCurrency
val member = charge.member!!
member.charge(charge.chargeCan, charge.rewardCan, "pg")
applicationEventPublisher.publishEvent(
ChargeSpringEvent(
chargeId = charge.id!!,
memberId = member.id!!
)
)
true
} else {
false
}
}
PaymentStatus.COMPLETE -> {
// 이미 결제가 완료된 경우 성공 처리(idempotent)
true
}
else -> {
// 그 외 상태는 404
false
}
}
}
@Transactional
fun chargeByCoupon(couponNumber: String, member: Member): String {
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
@@ -126,6 +231,177 @@ class ChargeService(
}
}
@Transactional
fun payverseCharge(member: Member, request: PayverseChargeRequest): PayverseChargeResponse {
val can = canRepository.findByIdOrNull(request.canId)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
val requestCurrency = can.currency
val isKrw = requestCurrency == "KRW"
val mid = if (isKrw) {
payverseMid
} else {
payverseUsdMid
}
val clientKey = if (isKrw) {
payverseClientKey
} else {
payverseUsdClientKey
}
val secretKey = if (isKrw) {
payverseSecretKey
} else {
payverseUsdSecretKey
}
val charge = Charge(can.can, can.rewardCan)
charge.title = can.title
charge.member = member
charge.can = can
val payment = Payment(paymentGateway = PaymentGateway.PAYVERSE)
payment.price = can.price
charge.payment = payment
val savedCharge = chargeRepository.save(charge)
val chargeId = savedCharge.id!!
val amount = BigDecimal(
savedCharge.payment!!.price
.setScale(4, RoundingMode.HALF_UP)
.stripTrailingZeros()
.toPlainString()
)
val reqDate = savedCharge.createdAt!!.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
val sign = DigestUtils.sha512Hex(
String.format(
"||%s||%s||%s||%s||%s||",
secretKey,
mid,
chargeId,
amount,
reqDate
)
)
val customerId = "${serverEnv}_user_${member.id!!}"
val payload = linkedMapOf(
"mid" to mid,
"clientKey" to clientKey,
"orderId" to chargeId.toString(),
"customerId" to customerId,
"productName" to can.title,
"requestCurrency" to requestCurrency,
"requestAmount" to amount,
"reqDate" to reqDate,
"sign" to sign
)
val payloadJson = objectMapper.writeValueAsString(payload)
return PayverseChargeResponse(chargeId = charge.id!!, payloadJson = payloadJson)
}
@Transactional
fun payverseVerify(memberId: Long, verifyRequest: PayverseVerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.")
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
val isKrw = charge.can?.currency == "KRW"
val mid = if (isKrw) {
payverseMid
} else {
payverseUsdMid
}
val clientKey = if (isKrw) {
payverseClientKey
} else {
payverseUsdClientKey
}
// 결제수단 확인
if (charge.payment?.paymentGateway != PaymentGateway.PAYVERSE) {
throw SodaException("결제정보에 오류가 있습니다.")
}
// 결제 상태에 따른 분기 처리
when (charge.payment?.status) {
PaymentStatus.REQUEST -> {
try {
val url = "$payverseHost/payment/search/transaction/${verifyRequest.transactionId}"
val request = Request.Builder()
.url(url)
.addHeader("mid", mid)
.addHeader("clientKey", clientKey)
.get()
.build()
val response = okHttpClient.newCall(request).execute()
if (!response.isSuccessful) {
throw SodaException("결제정보에 오류가 있습니다.")
}
val body = response.body?.string() ?: throw SodaException("결제정보에 오류가 있습니다.")
val verifyResponse = objectMapper.readValue(body, PayverseVerifyResponse::class.java)
val customerId = "${serverEnv}_user_${member.id!!}"
val isSuccess = verifyResponse.resultStatus == "SUCCESS" &&
verifyResponse.transactionStatus == "SUCCESS" &&
verifyResponse.orderId.toLongOrNull() == charge.id &&
verifyResponse.customerId == customerId &&
verifyResponse.requestAmount.compareTo(charge.can!!.price) == 0
if (isSuccess) {
// verify 함수의 232~248 라인과 동일 처리
charge.payment?.receiptId = verifyResponse.tid
val mappedMethod = if (verifyResponse.schemeGroup == "PVKR") {
mapPayverseSchemeToMethodByCode(verifyResponse.schemeCode)
} else {
null
}
charge.payment?.method = mappedMethod ?: verifyResponse.schemeCode
charge.payment?.status = PaymentStatus.COMPLETE
// 통화코드 설정
charge.payment?.locale = verifyResponse.requestCurrency
member.charge(charge.chargeCan, charge.rewardCan, "pg")
applicationEventPublisher.publishEvent(
ChargeSpringEvent(
chargeId = charge.id!!,
memberId = member.id!!
)
)
return ChargeCompleteResponse(
price = charge.payment!!.price,
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
}
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
}
}
PaymentStatus.COMPLETE -> {
// 이미 결제가 완료된 경우, 동일한 데이터로 즉시 반환
return ChargeCompleteResponse(
price = charge.payment!!.price,
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
}
else -> {
throw SodaException("결제정보에 오류가 있습니다.")
}
}
}
@Transactional
fun charge(member: Member, request: ChargeRequest): ChargeResponse {
val can = canRepository.findByIdOrNull(request.canId)
@@ -137,7 +413,7 @@ class ChargeService(
charge.can = can
val payment = Payment(paymentGateway = request.paymentGateway)
payment.price = can.price.toDouble()
payment.price = can.price
charge.payment = payment
chargeRepository.save(charge)
@@ -176,14 +452,14 @@ class ChargeService(
)
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
price = charge.payment!!.price,
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
}
} catch (e: Exception) {
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
}
} else {
@@ -226,14 +502,14 @@ class ChargeService(
)
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
price = charge.payment!!.price,
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
}
} catch (e: Exception) {
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
}
} else {
@@ -251,7 +527,7 @@ class ChargeService(
payment.price = if (request.price != null) {
request.price!!
} else {
0.toDouble()
0.toBigDecimal()
}
payment.locale = request.locale
@@ -286,7 +562,7 @@ class ChargeService(
)
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
price = charge.payment!!.price,
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
@@ -303,7 +579,7 @@ class ChargeService(
member: Member,
title: String,
chargeCan: Int,
price: Double,
price: BigDecimal,
currencyCode: String,
productId: String,
purchaseToken: String,
@@ -331,8 +607,7 @@ class ChargeService(
memberId: Long,
chargeId: Long,
productId: String,
purchaseToken: String,
paymentGateway: PaymentGateway
purchaseToken: String
): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(id = chargeId)
?: throw SodaException("결제정보에 오류가 있습니다.")
@@ -354,7 +629,7 @@ class ChargeService(
)
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
price = charge.payment!!.price,
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
@@ -436,4 +711,13 @@ class ChargeService(
throw SodaException("결제를 완료하지 못했습니다.")
}
}
// Payverse 결제수단 매핑: 특정 schemeCode는 "카드"로 표기, 아니면 null 반환
private fun mapPayverseSchemeToMethodByCode(schemeCode: String?): String? {
val cardCodes = setOf(
"041", "044", "361", "364", "365", "366", "367", "368", "369", "370", "371", "372", "373", "374", "381",
"218", "071", "002", "089", "045", "050", "048", "090", "092"
)
return if (schemeCode != null && cardCodes.contains(schemeCode)) "카드" else null
}
}

View File

@@ -1,9 +1,10 @@
package kr.co.vividnext.sodalive.can.charge.temp
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import java.math.BigDecimal
data class ChargeTempRequest(
val can: Int,
val price: Int,
val price: BigDecimal,
val paymentGateway: PaymentGateway
)

View File

@@ -41,7 +41,7 @@ class ChargeTempService(
charge.member = member
val payment = Payment(paymentGateway = request.paymentGateway)
payment.price = request.price.toDouble()
payment.price = request.price
charge.payment = payment
chargeRepository.save(charge)
@@ -66,7 +66,7 @@ class ChargeTempService(
VerifyResult::class.java
)
if (verifyResult.status == 1 && verifyResult.price == charge.payment!!.price.toInt()) {
if (verifyResult.status == 1 && verifyResult.price == charge.payment!!.price) {
charge.payment?.receiptId = verifyResult.receiptId
charge.payment?.method = verifyResult.method
charge.payment?.status = PaymentStatus.COMPLETE
@@ -74,7 +74,7 @@ class ChargeTempService(
} else {
throw SodaException("결제정보에 오류가 있습니다.")
}
} catch (e: Exception) {
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
}
} else {

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.can.payment
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.common.BaseEntity
import java.math.BigDecimal
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
@@ -25,7 +26,8 @@ data class Payment(
var receiptId: String? = null
var method: String? = null
var price: Double = 0.toDouble()
@Column(precision = 10, scale = 4, nullable = false)
var price: BigDecimal = 0.toBigDecimal()
var locale: String? = null
var orderId: String? = null
}

View File

@@ -1,5 +1,5 @@
package kr.co.vividnext.sodalive.can.payment
enum class PaymentGateway {
PG, GOOGLE_IAP, APPLE_IAP, POINT_CLICK_AD
PG, PAYVERSE, GOOGLE_IAP, APPLE_IAP, POINT_CLICK_AD
}

View File

@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.chat.original.controller
import kr.co.vividnext.sodalive.chat.character.dto.Character
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkCharactersPageResponse
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkDetailResponse
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListItemResponse
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListResponse
@@ -79,37 +78,4 @@ class OriginalWorkController(
val response = OriginalWorkDetailResponse.from(ow, imageHost, characters)
ApiResponse.ok(response)
}
/**
* 지정 원작에 속한 활성 캐릭터 목록 조회 (페이징)
* - 로그인 및 본인인증 필수
* - 기본 페이지 사이즈 20
*/
@GetMapping("/{id}/characters")
fun listCharacters(
@PathVariable id: Long,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
val pageRes = queryService.getActiveCharactersPage(id, page, size)
val content = pageRes.content.map {
val path = it.imagePath ?: "profile/default-profile.png"
Character(
characterId = it.id!!,
name = it.name,
description = it.description,
imageUrl = "$imageHost/$path"
)
}
ApiResponse.ok(
OriginalWorkCharactersPageResponse(
totalCount = pageRes.totalElements,
content = content
)
)
}
}

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.common
const val WAF_GEO_HEADER = "x-amzn-waf-geo-country"
enum class GeoCountry { KR, OTHER }
fun parseGeo(headerValue: String?): GeoCountry =
if (headerValue?.trim()?.uppercase() == "KR") GeoCountry.KR else GeoCountry.OTHER

View File

@@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.common
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class GeoCountryFilter : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val country = parseGeo(request.getHeader(WAF_GEO_HEADER))
request.setAttribute("geoCountry", country)
filterChain.doFilter(request, response)
}
}

View File

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.multipart.MaxUploadSizeExceededException
import org.springframework.web.server.ResponseStatusException
@RestControllerAdvice
class SodaExceptionHandler {
@@ -63,6 +64,7 @@ class SodaExceptionHandler {
@ExceptionHandler(Exception::class)
fun handleException(e: Exception) = run {
if (e is ResponseStatusException) throw e
logger.error("API error", e)
ApiResponse.error("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}

View File

@@ -96,6 +96,7 @@ class SecurityConfig(
.antMatchers(HttpMethod.GET, "/api/chat/character/main").permitAll()
.antMatchers(HttpMethod.GET, "/api/chat/room/list").permitAll()
.antMatchers(HttpMethod.GET, "/api/chat/original/list").permitAll()
.antMatchers(HttpMethod.POST, "/charge/payverse/webhook").permitAll()
.anyRequest().authenticated()
.and()
.build()

View File

@@ -53,7 +53,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
@@ -119,7 +122,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
@@ -196,7 +202,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
audioContent.member.id.eq(memberId)
.and(order.isActive.isTrue)
@@ -318,7 +327,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))

View File

@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.marketing
import java.math.BigDecimal
import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
@@ -19,7 +21,8 @@ data class AdTrackingHistory(
val pidName: String,
@Enumerated(value = EnumType.STRING)
val type: AdTrackingHistoryType,
val price: Double = 0.toDouble(),
@Column(precision = 10, scale = 4, nullable = false)
val price: BigDecimal = 0.toBigDecimal(),
val locale: String? = null,
val memberId: Long,
val createdAt: LocalDateTime = LocalDateTime.now(),

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.springframework.stereotype.Service
import java.math.BigDecimal
@Service
class AdTrackingService(
@@ -17,7 +18,7 @@ class AdTrackingService(
pid: String,
type: AdTrackingHistoryType,
memberId: Long,
price: Double? = null,
price: BigDecimal? = null,
locale: String? = null
) {
coroutineScope.launch {
@@ -30,7 +31,7 @@ class AdTrackingService(
pid = pid,
pidName = mediaPartner.pidName,
type = type,
price = price ?: 0.toDouble(),
price = price ?: 0.toBigDecimal(),
locale = locale,
memberId = memberId
)

View File

@@ -1,6 +1,7 @@
server:
shutdown: graceful
env: ${SERVER_ENV}
forward-headers-strategy: framework
logging:
level:
@@ -13,6 +14,16 @@ weraser:
apiUrl: ${WERASER_API_URL}
apiKey: ${WERASER_API_KEY}
payverse:
host: ${PAYVERSE_HOST}
inboundIp: ${PAYVERSE_INBOUND_IP}
mid: ${PAYVERSE_MID}
clientKey: ${PAYVERSE_CLIENT_KEY}
secretKey: ${PAYVERSE_SECRET_KEY}
usdMid: ${PAYVERSE_USD_MID}
usdClientKey: ${PAYVERSE_USD_CLIENT_KEY}
usdSecretKey: ${PAYVERSE_USD_SECRET_KEY}
bootpay:
applicationId: ${BOOTPAY_APPLICATION_ID}
privateKey: ${BOOTPAY_PRIVATE_KEY}