From 9b0d1b43d5493cf0d48119ad2502d7290839df20 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 9 Jan 2026 11:51:42 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EC=BA=94=20=EC=82=AC=EC=9A=A9=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B5=AD=EA=B0=80=20=EC=BD=94=EB=93=9C=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CloudFront-Viewer-Country 헤더를 통해 국가 코드를 수집하고 캔 사용 내역(UseCan) 저장 시 함께 기록하도록 수정 요청별 국가 정보 관리를 위한 컨텍스트와 인터셉터를 구현 --- .../sodalive/can/payment/CanPaymentService.kt | 13 ++++++++---- .../co/vividnext/sodalive/can/use/UseCan.kt | 4 +++- .../sodalive/common/CountryContext.kt | 15 +++++++++++++ .../sodalive/common/CountryInterceptor.kt | 21 +++++++++++++++++++ .../vividnext/sodalive/configs/WebConfig.kt | 5 ++++- 5 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/CountryContext.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/CountryInterceptor.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt index ffc8101b..8a93aaa7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -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.UseCanRepository 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.content.AudioContent import kr.co.vividnext.sodalive.content.order.Order @@ -35,7 +36,8 @@ class CanPaymentService( private val useCanRepository: UseCanRepository, private val useCanCalculateRepository: UseCanCalculateRepository, private val messageSource: SodaMessageSource, - private val langContext: LangContext + private val langContext: LangContext, + private val countryContext: CountryContext ) { @Transactional fun spendCan( @@ -76,7 +78,8 @@ class CanPaymentService( canUsage = canUsage, can = useChargeCan?.total ?: 0, rewardCan = useRewardCan.total, - isSecret = isSecret + isSecret = isSecret, + countryCode = countryContext.countryCode ) var recipientId: Long? = null @@ -378,7 +381,8 @@ class CanPaymentService( canUsage = CanUsage.CHARACTER_IMAGE_PURCHASE, can = useChargeCan?.total ?: 0, rewardCan = useRewardCan.total, - isSecret = false + isSecret = false, + countryCode = countryContext.countryCode ) useCan.member = member useCan.characterImage = image @@ -424,7 +428,8 @@ class CanPaymentService( canUsage = CanUsage.CHAT_MESSAGE_PURCHASE, can = useChargeCan?.total ?: 0, rewardCan = useRewardCan.total, - isSecret = false + isSecret = false, + countryCode = countryContext.countryCode ) useCan.member = member useCan.chatMessage = message diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt index dfb0b2ba..f2af2e2b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt @@ -34,7 +34,9 @@ data class UseCan( // 채팅 연동을 위한 식별자 (옵션) var chatRoomId: Long? = null, - var characterId: Long? = null + var characterId: Long? = null, + + var countryCode: String? = null ) : BaseEntity() { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/CountryContext.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/CountryContext.kt new file mode 100644 index 00000000..5c71f37a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/CountryContext.kt @@ -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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/CountryInterceptor.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/CountryInterceptor.kt new file mode 100644 index 00000000..9c1bf24d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/CountryInterceptor.kt @@ -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 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt index 6ef72f95..fe0b9c9e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.configs +import kr.co.vividnext.sodalive.common.CountryInterceptor import kr.co.vividnext.sodalive.i18n.LangInterceptor import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.CorsRegistry @@ -8,10 +9,12 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration class WebConfig( - private val langInterceptor: LangInterceptor + private val langInterceptor: LangInterceptor, + private val countryInterceptor: CountryInterceptor ) : WebMvcConfigurer { override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(langInterceptor).addPathPatterns("/**") + registry.addInterceptor(countryInterceptor).addPathPatterns("/**") } override fun addCorsMappings(registry: CorsRegistry) { From aa9a0bbe824026d96d1f927e4a56c8cb3d43e441 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 9 Jan 2026 11:54:21 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EC=BA=94=20=EC=82=AC=EC=9A=A9=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B5=AD=EA=B0=80=20=EC=BD=94=EB=93=9C=EC=97=90=20=EC=96=B4?= =?UTF-8?q?=EB=96=A4=20=ED=91=9C=EC=A4=80=20=EA=B5=AD=EA=B0=80=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=9D=B8=EC=A7=80=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt index f2af2e2b..95a2116e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt @@ -36,6 +36,7 @@ data class UseCan( var chatRoomId: Long? = null, var characterId: Long? = null, + // ISO 3166-1 alpha-2 국가 코드 var countryCode: String? = null ) : BaseEntity() { @ManyToOne(fetch = FetchType.LAZY) From 435010d5238202a4a8b99012deb6696ba7b6608a Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 12 Jan 2026 11:03:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=BD=98=ED=85=90=EC=B8=A0=20-=20=EB=B3=B8?= =?UTF-8?q?=EC=9D=B8(=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0)?= =?UTF-8?q?=EB=A7=8C=20=EC=98=A4=ED=94=88=EC=98=88=EC=A0=95=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/AudioContentRepository.kt | 26 ++++++++++++++++--- .../sodalive/content/AudioContentService.kt | 3 +++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index 242779e9..d30632e9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -44,6 +44,7 @@ interface AudioContentQueryRepository { fun findByIdAndCreatorId(contentId: Long, creatorId: Long): AudioContent? fun findByCreatorId( creatorId: Long, + isCreator: Boolean = false, coverImageHost: String, isAdult: Boolean = false, contentType: ContentType = ContentType.ALL, @@ -55,6 +56,7 @@ interface AudioContentQueryRepository { fun findTotalCountByCreatorId( creatorId: Long, + isCreator: Boolean = false, isAdult: Boolean = false, categoryId: Long = 0, contentType: ContentType = ContentType.ALL @@ -230,6 +232,7 @@ class AudioContentQueryRepositoryImpl( override fun findByCreatorId( creatorId: Long, + isCreator: Boolean, coverImageHost: String, isAdult: Boolean, contentType: ContentType, @@ -246,11 +249,18 @@ class AudioContentQueryRepositoryImpl( } 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 .and(audioContent.duration.isNotNull) - .or(audioContent.releaseDate.isNotNull.and(audioContent.duration.isNotNull)) ) + } if (!isAdult) { where = where.and(audioContent.isAdult.isFalse) @@ -332,16 +342,24 @@ class AudioContentQueryRepositoryImpl( override fun findTotalCountByCreatorId( creatorId: Long, + isCreator: Boolean, isAdult: Boolean, categoryId: Long, contentType: ContentType ): Int { 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 .and(audioContent.duration.isNotNull) - .or(audioContent.releaseDate.isNotNull.and(audioContent.duration.isNotNull)) ) + } if (!isAdult) { where = where.and(audioContent.isAdult.isFalse) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index a61a2af6..fdd2fcd9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -979,9 +979,11 @@ class AudioContentService( limit: Long ): GetAudioContentListResponse { val isAdult = member.auth != null && isAdultContentVisible + val isCreator = member.id == creatorId val totalCount = repository.findTotalCountByCreatorId( creatorId = creatorId, + isCreator = isCreator, isAdult = isAdult, categoryId = categoryId, contentType = contentType @@ -989,6 +991,7 @@ class AudioContentService( val audioContentList = repository.findByCreatorId( creatorId = creatorId, + isCreator = isCreator, coverImageHost = coverImageHost, isAdult = isAdult, contentType = contentType, From 6683b40425c9c2567a31744a0ab5feee9aff8ee1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 12 Jan 2026 11:24:51 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=BD=98=ED=85=90=EC=B8=A0=20-=20=EB=B3=B8?= =?UTF-8?q?=EC=9D=B8(=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0)?= =?UTF-8?q?=EB=A7=8C=20=EC=98=A4=ED=94=88=EC=98=88=EC=A0=95=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/explorer/ExplorerQueryRepository.kt | 13 +++++++++++++ .../vividnext/sodalive/explorer/ExplorerService.kt | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt index aac21c9b..d11e69d6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -38,6 +38,7 @@ import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository +import java.math.BigDecimal import java.time.Duration import java.time.LocalDate import java.time.LocalDateTime @@ -651,6 +652,18 @@ class ExplorerQueryRepository( .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 { // 활성 주문 + 대여의 경우 유효기간 내 주문만 포함, 동일 콘텐츠 중복 구매는 1개로 카운트 return queryFactory diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt index 53551728..71d087ba 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -287,9 +287,9 @@ class ExplorerService( null } - // 크리에이터의 전체 콘텐츠 개수 + // 크리에이터의 전체 유료 콘텐츠 개수 val totalContentCount = if (isCreator) { - queryRepository.getContentCount(creatorId) ?: 0 + queryRepository.getPaidContentCount(creatorId) ?: 0 } else { 0 }