캔 사용 시 국가 코드 기록 기능 추가 #374

Merged
klaus merged 4 commits from test into main 2026-01-12 02:31:47 +00:00
9 changed files with 93 additions and 12 deletions

View File

@@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
import kr.co.vividnext.sodalive.can.use.UseCanRepository import kr.co.vividnext.sodalive.can.use.UseCanRepository
import kr.co.vividnext.sodalive.chat.character.image.CharacterImage import kr.co.vividnext.sodalive.chat.character.image.CharacterImage
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.AudioContent import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.order.Order import kr.co.vividnext.sodalive.content.order.Order
@@ -35,7 +36,8 @@ class CanPaymentService(
private val useCanRepository: UseCanRepository, private val useCanRepository: UseCanRepository,
private val useCanCalculateRepository: UseCanCalculateRepository, private val useCanCalculateRepository: UseCanCalculateRepository,
private val messageSource: SodaMessageSource, private val messageSource: SodaMessageSource,
private val langContext: LangContext private val langContext: LangContext,
private val countryContext: CountryContext
) { ) {
@Transactional @Transactional
fun spendCan( fun spendCan(
@@ -76,7 +78,8 @@ class CanPaymentService(
canUsage = canUsage, canUsage = canUsage,
can = useChargeCan?.total ?: 0, can = useChargeCan?.total ?: 0,
rewardCan = useRewardCan.total, rewardCan = useRewardCan.total,
isSecret = isSecret isSecret = isSecret,
countryCode = countryContext.countryCode
) )
var recipientId: Long? = null var recipientId: Long? = null
@@ -378,7 +381,8 @@ class CanPaymentService(
canUsage = CanUsage.CHARACTER_IMAGE_PURCHASE, canUsage = CanUsage.CHARACTER_IMAGE_PURCHASE,
can = useChargeCan?.total ?: 0, can = useChargeCan?.total ?: 0,
rewardCan = useRewardCan.total, rewardCan = useRewardCan.total,
isSecret = false isSecret = false,
countryCode = countryContext.countryCode
) )
useCan.member = member useCan.member = member
useCan.characterImage = image useCan.characterImage = image
@@ -424,7 +428,8 @@ class CanPaymentService(
canUsage = CanUsage.CHAT_MESSAGE_PURCHASE, canUsage = CanUsage.CHAT_MESSAGE_PURCHASE,
can = useChargeCan?.total ?: 0, can = useChargeCan?.total ?: 0,
rewardCan = useRewardCan.total, rewardCan = useRewardCan.total,
isSecret = false isSecret = false,
countryCode = countryContext.countryCode
) )
useCan.member = member useCan.member = member
useCan.chatMessage = message useCan.chatMessage = message

View File

@@ -34,7 +34,10 @@ data class UseCan(
// 채팅 연동을 위한 식별자 (옵션) // 채팅 연동을 위한 식별자 (옵션)
var chatRoomId: Long? = null, var chatRoomId: Long? = null,
var characterId: Long? = null var characterId: Long? = null,
// ISO 3166-1 alpha-2 국가 코드
var countryCode: String? = null
) : BaseEntity() { ) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false) @JoinColumn(name = "member_id", nullable = false)

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.common
import org.springframework.stereotype.Component
import org.springframework.web.context.annotation.RequestScope
@Component
@RequestScope
class CountryContext {
var countryCode: String? = null
internal set
fun setCountryCode(code: String?) {
this.countryCode = code
}
}

View File

@@ -0,0 +1,21 @@
package kr.co.vividnext.sodalive.common
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class CountryInterceptor(
private val countryContext: CountryContext
) : HandlerInterceptor {
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
val countryCode = request.getHeader("CloudFront-Viewer-Country")
countryContext.setCountryCode(countryCode)
return true
}
}

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.configs package kr.co.vividnext.sodalive.configs
import kr.co.vividnext.sodalive.common.CountryInterceptor
import kr.co.vividnext.sodalive.i18n.LangInterceptor import kr.co.vividnext.sodalive.i18n.LangInterceptor
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.CorsRegistry
@@ -8,10 +9,12 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration @Configuration
class WebConfig( class WebConfig(
private val langInterceptor: LangInterceptor private val langInterceptor: LangInterceptor,
private val countryInterceptor: CountryInterceptor
) : WebMvcConfigurer { ) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) { override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(langInterceptor).addPathPatterns("/**") registry.addInterceptor(langInterceptor).addPathPatterns("/**")
registry.addInterceptor(countryInterceptor).addPathPatterns("/**")
} }
override fun addCorsMappings(registry: CorsRegistry) { override fun addCorsMappings(registry: CorsRegistry) {

View File

@@ -44,6 +44,7 @@ interface AudioContentQueryRepository {
fun findByIdAndCreatorId(contentId: Long, creatorId: Long): AudioContent? fun findByIdAndCreatorId(contentId: Long, creatorId: Long): AudioContent?
fun findByCreatorId( fun findByCreatorId(
creatorId: Long, creatorId: Long,
isCreator: Boolean = false,
coverImageHost: String, coverImageHost: String,
isAdult: Boolean = false, isAdult: Boolean = false,
contentType: ContentType = ContentType.ALL, contentType: ContentType = ContentType.ALL,
@@ -55,6 +56,7 @@ interface AudioContentQueryRepository {
fun findTotalCountByCreatorId( fun findTotalCountByCreatorId(
creatorId: Long, creatorId: Long,
isCreator: Boolean = false,
isAdult: Boolean = false, isAdult: Boolean = false,
categoryId: Long = 0, categoryId: Long = 0,
contentType: ContentType = ContentType.ALL contentType: ContentType = ContentType.ALL
@@ -230,6 +232,7 @@ class AudioContentQueryRepositoryImpl(
override fun findByCreatorId( override fun findByCreatorId(
creatorId: Long, creatorId: Long,
isCreator: Boolean,
coverImageHost: String, coverImageHost: String,
isAdult: Boolean, isAdult: Boolean,
contentType: ContentType, contentType: ContentType,
@@ -246,11 +249,18 @@ class AudioContentQueryRepositoryImpl(
} }
var where = audioContent.member.id.eq(creatorId) var where = audioContent.member.id.eq(creatorId)
.and(
where = if (isCreator) {
where.and(
audioContent.releaseDate.isNotNull
.and(audioContent.duration.isNotNull)
)
} else {
where.and(
audioContent.isActive.isTrue audioContent.isActive.isTrue
.and(audioContent.duration.isNotNull) .and(audioContent.duration.isNotNull)
.or(audioContent.releaseDate.isNotNull.and(audioContent.duration.isNotNull))
) )
}
if (!isAdult) { if (!isAdult) {
where = where.and(audioContent.isAdult.isFalse) where = where.and(audioContent.isAdult.isFalse)
@@ -332,16 +342,24 @@ class AudioContentQueryRepositoryImpl(
override fun findTotalCountByCreatorId( override fun findTotalCountByCreatorId(
creatorId: Long, creatorId: Long,
isCreator: Boolean,
isAdult: Boolean, isAdult: Boolean,
categoryId: Long, categoryId: Long,
contentType: ContentType contentType: ContentType
): Int { ): Int {
var where = audioContent.member.id.eq(creatorId) var where = audioContent.member.id.eq(creatorId)
.and(
where = if (isCreator) {
where.and(
audioContent.releaseDate.isNotNull
.and(audioContent.duration.isNotNull)
)
} else {
where.and(
audioContent.isActive.isTrue audioContent.isActive.isTrue
.and(audioContent.duration.isNotNull) .and(audioContent.duration.isNotNull)
.or(audioContent.releaseDate.isNotNull.and(audioContent.duration.isNotNull))
) )
}
if (!isAdult) { if (!isAdult) {
where = where.and(audioContent.isAdult.isFalse) where = where.and(audioContent.isAdult.isFalse)

View File

@@ -979,9 +979,11 @@ class AudioContentService(
limit: Long limit: Long
): GetAudioContentListResponse { ): GetAudioContentListResponse {
val isAdult = member.auth != null && isAdultContentVisible val isAdult = member.auth != null && isAdultContentVisible
val isCreator = member.id == creatorId
val totalCount = repository.findTotalCountByCreatorId( val totalCount = repository.findTotalCountByCreatorId(
creatorId = creatorId, creatorId = creatorId,
isCreator = isCreator,
isAdult = isAdult, isAdult = isAdult,
categoryId = categoryId, categoryId = categoryId,
contentType = contentType contentType = contentType
@@ -989,6 +991,7 @@ class AudioContentService(
val audioContentList = repository.findByCreatorId( val audioContentList = repository.findByCreatorId(
creatorId = creatorId, creatorId = creatorId,
isCreator = isCreator,
coverImageHost = coverImageHost, coverImageHost = coverImageHost,
isAdult = isAdult, isAdult = isAdult,
contentType = contentType, contentType = contentType,

View File

@@ -38,6 +38,7 @@ import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag
import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.math.BigDecimal
import java.time.Duration import java.time.Duration
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -651,6 +652,18 @@ class ExplorerQueryRepository(
.fetchFirst() .fetchFirst()
} }
fun getPaidContentCount(creatorId: Long): Long? {
return queryFactory
.select(audioContent.id.count())
.from(audioContent)
.where(
audioContent.isActive.isTrue
.and(audioContent.member.id.eq(creatorId))
.and(audioContent.price.gt(BigDecimal.ZERO))
)
.fetchFirst()
}
fun getOwnedContentCount(creatorId: Long, memberId: Long): Long { fun getOwnedContentCount(creatorId: Long, memberId: Long): Long {
// 활성 주문 + 대여의 경우 유효기간 내 주문만 포함, 동일 콘텐츠 중복 구매는 1개로 카운트 // 활성 주문 + 대여의 경우 유효기간 내 주문만 포함, 동일 콘텐츠 중복 구매는 1개로 카운트
return queryFactory return queryFactory

View File

@@ -287,9 +287,9 @@ class ExplorerService(
null null
} }
// 크리에이터의 전체 콘텐츠 개수 // 크리에이터의 전체 유료 콘텐츠 개수
val totalContentCount = if (isCreator) { val totalContentCount = if (isCreator) {
queryRepository.getContentCount(creatorId) ?: 0 queryRepository.getPaidContentCount(creatorId) ?: 0
} else { } else {
0 0
} }