캔 결제 메시지 다국어 처리

This commit is contained in:
2025-12-23 18:09:17 +09:00
parent 58f7a8654b
commit 6e8a88178c
11 changed files with 358 additions and 110 deletions

View File

@@ -27,7 +27,7 @@ class CanController(private val service: CanService) {
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.getCanStatus(member, container))
@@ -41,7 +41,7 @@ class CanController(private val service: CanService) {
pageable: Pageable
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.getCanUseStatus(member, pageable, timezone, container))
@@ -55,7 +55,7 @@ class CanController(private val service: CanService) {
pageable: Pageable
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.getCanChargeStatus(member, pageable, timezone, container))

View File

@@ -33,7 +33,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.payverseCharge(member, request))
@@ -45,7 +45,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
val response = service.payverseVerify(memberId = member.id!!, verifyRequest)
@@ -83,7 +83,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.charge(member, chargeRequest))
@@ -95,7 +95,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
val response = service.verify(memberId = member.id!!, verifyRequest)
@@ -109,7 +109,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
val response = service.verifyHecto(memberId = member.id!!, verifyRequest)
@@ -123,7 +123,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.appleCharge(member, chargeRequest))
@@ -135,7 +135,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
val response = service.appleVerify(memberId = member.id!!, verifyRequest)
@@ -149,7 +149,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
if (request.paymentGateway == PaymentGateway.GOOGLE_IAP) {
@@ -174,7 +174,7 @@ class ChargeController(
trackingCharge(member, response)
ApiResponse.ok(Unit)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}

View File

@@ -11,6 +11,8 @@ 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.google.GooglePlayService
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.point.MemberPoint
@@ -53,6 +55,8 @@ class ChargeService(
private val applicationEventPublisher: ApplicationEventPublisher,
private val googlePlayService: GooglePlayService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${bootpay.application-id}")
private val bootpayApplicationId: String,
@@ -174,10 +178,10 @@ class ChargeService(
@Transactional
fun chargeByCoupon(couponNumber: String, member: Member): String {
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.")
?: throw SodaException(messageKey = "can.coupon.invalid_number_contact")
if (canCouponNumber.member != null) {
throw SodaException("이미 사용한 쿠폰번호 입니다.")
throw SodaException(messageKey = "can.coupon.already_used")
}
canCouponNumber.member = member
@@ -186,7 +190,7 @@ class ChargeService(
when (coupon.couponType) {
CouponType.CAN -> {
val couponCharge = Charge(0, coupon.can, status = ChargeStatus.COUPON)
couponCharge.title = "${coupon.can}"
couponCharge.title = formatMessage("can.charge.title", coupon.can)
couponCharge.member = member
val payment = Payment(
@@ -198,7 +202,7 @@ class ChargeService(
chargeRepository.save(couponCharge)
member.charge(0, coupon.can, "pg")
return "쿠폰 사용이 완료되었습니다.\n${coupon.can}캔이 지급되었습니다."
return formatMessage("can.coupon.use_complete", coupon.can)
}
CouponType.POINT -> {
@@ -226,7 +230,7 @@ class ChargeService(
)
)
return "쿠폰 사용이 완료되었습니다.\n${coupon.can}포인트가 지급되었습니다."
return formatMessage("can.coupon.use_complete_point", coupon.can)
}
}
}
@@ -234,7 +238,7 @@ class ChargeService(
@Transactional
fun payverseCharge(member: Member, request: PayverseChargeRequest): PayverseChargeResponse {
val can = canRepository.findByIdOrNull(request.canId)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
?: throw SodaException(messageKey = "can.charge.invalid_request_restart")
val requestCurrency = can.currency
val isKrw = requestCurrency == "KRW"
@@ -304,9 +308,9 @@ class ChargeService(
@Transactional
fun payverseVerify(memberId: Long, verifyRequest: PayverseVerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.")
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
?: throw SodaException(messageKey = "common.error.bad_credentials")
val isKrw = charge.can?.currency == "KRW"
val mid = if (isKrw) {
@@ -322,7 +326,7 @@ class ChargeService(
// 결제수단 확인
if (charge.payment?.paymentGateway != PaymentGateway.PAYVERSE) {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
// 결제 상태에 따른 분기 처리
@@ -339,10 +343,11 @@ class ChargeService(
val response = okHttpClient.newCall(request).execute()
if (!response.isSuccessful) {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
val body = response.body?.string() ?: throw SodaException("결제정보에 오류가 있습니다.")
val body = response.body?.string()
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val verifyResponse = objectMapper.readValue(body, PayverseVerifyResponse::class.java)
val customerId = "${serverEnv}_user_${member.id!!}"
@@ -380,10 +385,10 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
@@ -397,7 +402,7 @@ class ChargeService(
}
else -> {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
}
@@ -405,7 +410,7 @@ class ChargeService(
@Transactional
fun charge(member: Member, request: ChargeRequest): ChargeResponse {
val can = canRepository.findByIdOrNull(request.canId)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
?: throw SodaException(messageKey = "can.charge.invalid_request_restart")
val charge = Charge(can.can, can.rewardCan)
charge.title = can.title
@@ -424,9 +429,9 @@ class ChargeService(
@Transactional
fun verify(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.")
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)
@@ -457,22 +462,22 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
@Transactional
fun verifyHecto(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.")
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
val bootpay = Bootpay(bootpayHectoApplicationId, bootpayHectoPrivateKey)
@@ -507,13 +512,13 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
@@ -542,15 +547,17 @@ class ChargeService(
@Transactional
fun appleVerify(memberId: Long, verifyRequest: AppleVerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId)
?: throw SodaException("결제정보에 오류가 있습니다.")
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) {
// 검증로직
if (requestRealServerVerify(verifyRequest)) {
charge.payment?.receiptId = verifyRequest.receiptString
charge.payment?.method = "애플(인 앱 결제)"
charge.payment?.method = messageSource
.getMessage("can.charge.payment_method.apple_iap", langContext.lang)
.orEmpty()
charge.payment?.status = PaymentStatus.COMPLETE
member.charge(charge.chargeCan, charge.rewardCan, "ios")
@@ -567,10 +574,10 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
@@ -594,7 +601,9 @@ class ChargeService(
payment.locale = currencyCode
payment.price = price
payment.receiptId = purchaseToken
payment.method = "구글(인 앱 결제)"
payment.method = messageSource
.getMessage("can.charge.payment_method.google_iap", langContext.lang)
.orEmpty()
charge.payment = payment
chargeRepository.save(charge)
@@ -610,9 +619,9 @@ class ChargeService(
purchaseToken: String
): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(id = chargeId)
?: throw SodaException("결제정보에 오류가 있습니다.")
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.status == PaymentStatus.REQUEST) {
val orderId = verifyPurchase(purchaseToken, productId)
@@ -634,10 +643,10 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else {
throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요")
throw SodaException(messageKey = "can.charge.purchase_failed_contact")
}
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
@@ -670,14 +679,14 @@ class ChargeService(
}
else -> {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
} else {
throw SodaException("결제를 완료하지 못했습니다.")
throw SodaException(messageKey = "can.charge.payment_incomplete")
}
} else {
throw SodaException("결제를 완료하지 못했습니다.")
throw SodaException(messageKey = "can.charge.payment_incomplete")
}
}
@@ -701,23 +710,31 @@ class ChargeService(
}
else -> {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
} else {
throw SodaException("결제를 완료하지 못했습니다.")
throw SodaException(messageKey = "can.charge.payment_incomplete")
}
} else {
throw SodaException("결제를 완료하지 못했습니다.")
throw SodaException(messageKey = "can.charge.payment_incomplete")
}
}
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
return String.format(template, *args)
}
// 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
if (schemeCode == null || !cardCodes.contains(schemeCode)) {
return null
}
return messageSource.getMessage("can.charge.payment_method.card", langContext.lang)
}
}

View File

@@ -9,6 +9,8 @@ import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.auth.AuthRepository
@@ -26,15 +28,17 @@ class ChargeEventService(
private val memberRepository: MemberRepository,
private val chargeRepository: ChargeRepository,
private val chargeEventRepository: ChargeEventRepository,
private val applicationEventPublisher: ApplicationEventPublisher
private val applicationEventPublisher: ApplicationEventPublisher,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
@Transactional
fun applyChargeEvent(chargeId: Long, memberId: Long) {
val charge = chargeRepository.findByIdOrNull(chargeId)
?: throw SodaException("이벤트가 적용되지 않았습니다.\n고객센터에 문의해 주세요.")
?: throw SodaException(messageKey = "can.charge.event.not_applied_contact")
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("이벤트가 적용되지 않았습니다.\n고객센터에 문의해 주세요.")
?: throw SodaException(messageKey = "can.charge.event.not_applied_contact")
if (member.auth != null) {
val authDate = authRepository.getOldestCreatedAtByDi(member.auth!!.di)
@@ -79,7 +83,10 @@ class ChargeEventService(
FcmEvent(
type = FcmEventType.INDIVIDUAL,
title = chargeEvent.title,
message = "$additionalCan 캔이 추가 지급되었습니다.",
message = formatMessage(
"can.charge.event.additional_can_paid",
additionalCan
),
recipients = listOf(member.id!!),
isAuth = null
)
@@ -94,14 +101,21 @@ class ChargeEventService(
additionalCan = additionalCan,
member = member,
paymentGateway = charge.payment?.paymentGateway!!,
method = "첫 충전 이벤트"
method = messageSource
.getMessage("can.charge.event.first_title", langContext.lang)
.orEmpty()
)
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.INDIVIDUAL,
title = "첫 충전 이벤트",
message = "$additionalCan 캔이 추가 지급되었습니다.",
title = messageSource
.getMessage("can.charge.event.first_title", langContext.lang)
.orEmpty(),
message = formatMessage(
"can.charge.event.additional_can_paid",
additionalCan
),
recipients = listOf(member.id!!),
isAuth = null
)
@@ -110,7 +124,7 @@ class ChargeEventService(
private fun applyEvent(additionalCan: Int, member: Member, paymentGateway: PaymentGateway, method: String) {
val eventCharge = Charge(0, additionalCan, status = ChargeStatus.EVENT)
eventCharge.title = "$additionalCan"
eventCharge.title = formatMessage("can.charge.title", additionalCan)
eventCharge.member = member
val payment = Payment(
@@ -127,4 +141,9 @@ class ChargeEventService(
else -> member.charge(0, additionalCan, "pg")
}
}
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
return String.format(template, *args)
}
}

View File

@@ -20,7 +20,7 @@ class ChargeTempController(private val service: ChargeTempService) {
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
throw SodaException(messageKey = "common.error.bad_credentials")
}
ApiResponse.ok(service.charge(member, request))

View File

@@ -12,6 +12,8 @@ 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 kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.beans.factory.annotation.Value
@@ -27,6 +29,8 @@ class ChargeTempService(
private val memberRepository: MemberRepository,
private val objectMapper: ObjectMapper,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${bootpay.hecto-application-id}")
private val bootpayApplicationId: String,
@@ -37,7 +41,7 @@ class ChargeTempService(
@Transactional
fun charge(member: Member, request: ChargeTempRequest): ChargeResponse {
val charge = Charge(request.can, 0)
charge.title = "${request.can.moneyFormat()}"
charge.title = formatMessage("can.charge.title", request.can.moneyFormat())
charge.member = member
val payment = Payment(paymentGateway = request.paymentGateway)
@@ -52,9 +56,9 @@ class ChargeTempService(
@Transactional
fun verify(user: User, verifyRequest: VerifyRequest) {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.")
?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByEmail(user.username)
?: throw SodaException("로그인 정보를 확인해주세요.")
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)
@@ -72,13 +76,18 @@ class ChargeTempService(
charge.payment?.status = PaymentStatus.COMPLETE
member.charge(charge.chargeCan, charge.rewardCan, "pg")
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
} else {
throw SodaException("결제정보에 오류가 있습니다.")
throw SodaException(messageKey = "can.charge.invalid_payment_info")
}
}
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
return String.format(template, *args)
}
}

View File

@@ -2,6 +2,8 @@ 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.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import org.springframework.core.io.InputStreamResource
import org.springframework.data.domain.Pageable
@@ -22,14 +24,18 @@ import java.nio.charset.StandardCharsets
@RestController
@RequestMapping("/can/coupon")
class CanCouponController(private val service: CanCouponService) {
class CanCouponController(
private val service: CanCouponService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun generateCoupon(
@RequestBody request: GenerateCanCouponRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.generateCoupon(request))
}
@@ -40,7 +46,7 @@ class CanCouponController(private val service: CanCouponService) {
@RequestBody request: ModifyCanCouponRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.modifyCoupon(request))
}
@@ -51,7 +57,7 @@ class CanCouponController(private val service: CanCouponService) {
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.getCouponList(offset = pageable.offset, limit = pageable.pageSize.toLong()))
}
@@ -63,7 +69,7 @@ class CanCouponController(private val service: CanCouponService) {
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(
service.getCouponNumberList(
@@ -80,9 +86,11 @@ class CanCouponController(private val service: CanCouponService) {
@RequestParam couponId: Long,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
val fileName = "쿠폰번호리스트.xlsx"
val fileName = messageSource
.getMessage("can.coupon.download_filename", langContext.lang)
.orEmpty()
val encodedFileName = URLEncoder.encode(
fileName,
StandardCharsets.UTF_8.toString()
@@ -107,7 +115,7 @@ class CanCouponController(private val service: CanCouponService) {
@RequestBody request: UseCanCouponRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
val completeMessage = service.useCanCoupon(
couponNumber = request.couponNumber,

View File

@@ -12,7 +12,7 @@ class CanCouponIssueService(private val couponNumberRepository: CanCouponNumberR
if (!isMultipleUse(canCouponNumber)) {
val canCouponNumberList = couponNumberRepository.findByMemberId(memberId = memberId)
if (canCouponNumberList.isNotEmpty()) {
throw SodaException("해당 쿠폰은 1회만 충전이 가능합니다.")
throw SodaException(messageKey = "can.coupon.single_use_only")
}
}
@@ -21,10 +21,10 @@ class CanCouponIssueService(private val couponNumberRepository: CanCouponNumberR
private fun checkCanCouponNumber(couponNumber: String): CanCouponNumber {
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.")
?: throw SodaException(messageKey = "can.coupon.invalid_number_contact")
if (canCouponNumber.member != null) {
throw SodaException("이미 사용한 쿠폰번호 입니다.")
throw SodaException(messageKey = "can.coupon.already_used")
}
return canCouponNumber
@@ -34,17 +34,17 @@ class CanCouponIssueService(private val couponNumberRepository: CanCouponNumberR
private fun validateCoupon(canCoupon: CanCoupon) {
if (canCoupon.validity < LocalDateTime.now()) {
throw SodaException("유효기간이 경과된 쿠폰입니다.")
throw SodaException(messageKey = "can.coupon.expired")
}
if (!canCoupon.isActive) {
throw SodaException("이용이 불가능한 쿠폰입니다.")
throw SodaException(messageKey = "can.coupon.inactive")
}
}
fun checkAnyChanges(request: ModifyCanCouponRequest) {
if (request.isMultipleUse == null && request.isActive == null && request.validity == null) {
throw SodaException("변경사항이 없습니다.")
throw SodaException(messageKey = "can.coupon.no_changes")
}
}
}

View File

@@ -5,6 +5,8 @@ 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.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.MemberRepository
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.context.ApplicationEventPublisher
@@ -29,7 +31,9 @@ class CanCouponService(
private val memberRepository: MemberRepository,
private val objectMapper: ObjectMapper,
private val applicationEventPublisher: ApplicationEventPublisher
private val applicationEventPublisher: ApplicationEventPublisher,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
fun generateCoupon(request: GenerateCanCouponRequest) {
val message = objectMapper.writeValueAsString(request)
@@ -41,7 +45,7 @@ class CanCouponService(
issueService.checkAnyChanges(request)
val canCoupon = repository.findByIdOrNull(id = request.couponId)
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.")
?: throw SodaException(messageKey = "can.coupon.invalid_number_contact")
if (request.validity != null) {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
@@ -51,7 +55,7 @@ class CanCouponService(
.toLocalDateTime()
if (validity <= canCoupon.validity) {
throw SodaException("유효기간은 기존 유효기간 이후 날짜로 설정하실 수 있습니다.")
throw SodaException(messageKey = "can.coupon.validity_after_current")
}
canCoupon.validity = validity
@@ -85,7 +89,11 @@ class CanCouponService(
}
fun downloadCouponNumberList(couponId: Long): ByteArrayInputStream {
val header = listOf("순번", "쿠폰번호", "사용여부")
val header = listOf(
messageSource.getMessage("can.coupon.download_header.index", langContext.lang).orEmpty(),
messageSource.getMessage("can.coupon.download_header.number", langContext.lang).orEmpty(),
messageSource.getMessage("can.coupon.download_header.used", langContext.lang).orEmpty()
)
val byteArrayOutputStream = ByteArrayOutputStream()
val couponNumberList = couponNumberRepository.getAllCouponNumberList(couponId)
@@ -104,9 +112,9 @@ class CanCouponService(
couponNumberRow.createCell(1).setCellValue(insertHyphens(item.couponNumber))
couponNumberRow.createCell(2).setCellValue(
if (item.isUsed) {
"O"
messageSource.getMessage("can.coupon.download_used_mark", langContext.lang).orEmpty()
} else {
"X"
messageSource.getMessage("can.coupon.download_unused_mark", langContext.lang).orEmpty()
}
)
}
@@ -114,7 +122,7 @@ class CanCouponService(
workbook.write(byteArrayOutputStream)
return ByteArrayInputStream(byteArrayOutputStream.toByteArray())
} catch (e: IOException) {
throw SodaException("다운로드를 하지 못했습니다.\n다시 시도해 주세요.")
throw SodaException(messageKey = "can.coupon.download_failed_retry")
} finally {
workbook.close()
byteArrayOutputStream.close()
@@ -123,9 +131,9 @@ class CanCouponService(
fun useCanCoupon(couponNumber: String, memberId: Long): String {
val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
?: throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("쿠폰은 본인인증을 하셔야 사용이 가능합니다.")
if (member.auth == null) throw SodaException(messageKey = "can.coupon.auth_required")
issueService.validateAvailableUseCoupon(couponNumber, memberId)

View File

@@ -18,6 +18,8 @@ import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.order.Order
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
@@ -31,7 +33,9 @@ class CanPaymentService(
private val memberRepository: MemberRepository,
private val chargeRepository: ChargeRepository,
private val useCanRepository: UseCanRepository,
private val useCanCalculateRepository: UseCanCalculateRepository
private val useCanCalculateRepository: UseCanCalculateRepository,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
@Transactional
fun spendCan(
@@ -49,7 +53,7 @@ class CanPaymentService(
container: String
) {
val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "can.payment.invalid_request_retry")
val useRewardCan = spendRewardCan(member, needCan, container)
val useChargeCan = if (needCan - useRewardCan.total > 0) {
spendChargeCan(member, needCan = needCan - useRewardCan.total, container = container)
@@ -58,14 +62,14 @@ class CanPaymentService(
}
if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) {
val shortCan = needCan - useRewardCan.total - (useChargeCan?.total ?: 0)
throw SodaException(
"${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " +
"캔이 부족합니다. 충전 후 이용해 주세요."
formatMessage("can.payment.insufficient_can", shortCan)
)
}
if (!useRewardCan.verify() || useChargeCan?.verify() == false) {
throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
throw SodaException(messageKey = "can.payment.invalid_request_retry")
}
val useCan = UseCan(
@@ -121,7 +125,7 @@ class CanPaymentService(
useCan.chatRoomId = chatRoomId
useCan.characterId = characterId
} else {
throw SodaException("잘못된 요청입니다.")
throw SodaException(messageKey = "common.error.invalid_request")
}
useCanRepository.save(useCan)
@@ -306,20 +310,20 @@ class CanPaymentService(
@Transactional
fun refund(memberId: Long, roomId: Long) {
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("잘못된 예약정보 입니다.")
?: throw SodaException(messageKey = "can.payment.invalid_reservation")
val useCan = repository.getCanUsedForLiveRoomNotRefund(
memberId = memberId,
roomId = roomId,
canUsage = CanUsage.LIVE
) ?: throw SodaException("잘못된 예약정보 입니다.")
) ?: throw SodaException(messageKey = "can.payment.invalid_reservation")
useCan.isRefund = true
val useCanCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!)
useCanCalculates.forEach {
it.status = UseCanCalculateStatus.REFUND
val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE)
charge.title = "${it.can}"
charge.title = formatMessage("can.charge.title", it.can)
charge.useCan = useCan
when (it.paymentGateway) {
@@ -333,7 +337,9 @@ class CanPaymentService(
status = PaymentStatus.COMPLETE,
paymentGateway = it.paymentGateway
)
payment.method = "환불"
payment.method = messageSource
.getMessage("can.payment.method.refund", langContext.lang)
.orEmpty()
charge.payment = payment
chargeRepository.save(charge)
@@ -348,7 +354,7 @@ class CanPaymentService(
container: String
) {
val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "can.payment.invalid_request_retry")
val useRewardCan = spendRewardCan(member, needCan, container)
val useChargeCan = if (needCan - useRewardCan.total > 0) {
@@ -358,14 +364,14 @@ class CanPaymentService(
}
if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) {
val shortCan = needCan - useRewardCan.total - (useChargeCan?.total ?: 0)
throw SodaException(
"${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " +
"캔이 부족합니다. 충전 후 이용해 주세요."
formatMessage("can.payment.insufficient_can", shortCan)
)
}
if (!useRewardCan.verify() || useChargeCan?.verify() == false) {
throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
throw SodaException(messageKey = "can.payment.invalid_request_retry")
}
val useCan = UseCan(
@@ -394,7 +400,7 @@ class CanPaymentService(
container: String
) {
val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
?: throw SodaException(messageKey = "can.payment.invalid_request_retry")
val useRewardCan = spendRewardCan(member, needCan, container)
val useChargeCan = if (needCan - useRewardCan.total > 0) {
@@ -404,14 +410,14 @@ class CanPaymentService(
}
if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) {
val shortCan = needCan - useRewardCan.total - (useChargeCan?.total ?: 0)
throw SodaException(
"${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " +
"캔이 부족합니다. 충전 후 이용해 주세요."
formatMessage("can.payment.insufficient_can", shortCan)
)
}
if (!useRewardCan.verify() || useChargeCan?.verify() == false) {
throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
throw SodaException(messageKey = "can.payment.invalid_request_retry")
}
val useCan = UseCan(
@@ -435,4 +441,9 @@ class CanPaymentService(
setUseCanCalculate(null, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.GOOGLE_IAP)
setUseCanCalculate(null, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.APPLE_IAP)
}
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
return String.format(template, *args)
}
}

View File

@@ -182,6 +182,178 @@ class SodaMessageSource {
)
)
private val canChargeMessages = mapOf(
"can.charge.invalid_payment_info" to mapOf(
Lang.KO to "결제정보에 오류가 있습니다.",
Lang.EN to "There is an error with the payment information.",
Lang.JA to "決済情報に誤りがあります。"
),
"can.charge.invalid_request_restart" to mapOf(
Lang.KO to "잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.",
Lang.EN to "Invalid request.\nPlease restart the app and try again.",
Lang.JA to "不正なリクエストです。\nアプリを終了して再度お試しください。"
),
"can.charge.purchase_failed_contact" to mapOf(
Lang.KO to "구매를 하지 못했습니다.\n고객센터로 문의해 주세요",
Lang.EN to "Purchase could not be completed.\nPlease contact customer support.",
Lang.JA to "購入を完了できませんでした。\nカスタマーサポートへお問い合わせください。"
),
"can.charge.payment_incomplete" to mapOf(
Lang.KO to "결제를 완료하지 못했습니다.",
Lang.EN to "Payment could not be completed.",
Lang.JA to "決済を完了できませんでした。"
),
"can.charge.payment_method.apple_iap" to mapOf(
Lang.KO to "애플(인 앱 결제)",
Lang.EN to "Apple (In-App Purchase)",
Lang.JA to "Appleアプリ内課金"
),
"can.charge.payment_method.google_iap" to mapOf(
Lang.KO to "구글(인 앱 결제)",
Lang.EN to "Google (In-App Purchase)",
Lang.JA to "Googleアプリ内課金"
),
"can.charge.payment_method.card" to mapOf(
Lang.KO to "카드",
Lang.EN to "Card",
Lang.JA to "カード"
),
"can.charge.title" to mapOf(
Lang.KO to "%s 캔",
Lang.EN to "%s cans",
Lang.JA to "%s缶"
)
)
private val canChargeEventMessages = mapOf(
"can.charge.event.not_applied_contact" to mapOf(
Lang.KO to "이벤트가 적용되지 않았습니다.\n고객센터에 문의해 주세요.",
Lang.EN to "The event was not applied.\nPlease contact customer support.",
Lang.JA to "イベントが適用されていません。\nカスタマーサポートへお問い合わせください。"
),
"can.charge.event.additional_can_paid" to mapOf(
Lang.KO to "%s 캔이 추가 지급되었습니다.",
Lang.EN to "%s cans have been added.",
Lang.JA to "%s缶が追加で支給されました。"
),
"can.charge.event.first_title" to mapOf(
Lang.KO to "첫 충전 이벤트",
Lang.EN to "First Recharge Event",
Lang.JA to "初回チャージイベント"
)
)
private val canCouponMessages = mapOf(
"can.coupon.invalid_number_contact" to mapOf(
Lang.KO to "잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.",
Lang.EN to "Invalid coupon number.\nPlease contact customer support.",
Lang.JA to "無効なクーポン番号です。\nカスタマーサポートへお問い合わせください。"
),
"can.coupon.already_used" to mapOf(
Lang.KO to "이미 사용한 쿠폰번호 입니다.",
Lang.EN to "This coupon number has already been used.",
Lang.JA to "すでに使用されたクーポン番号です。"
),
"can.coupon.use_complete" to mapOf(
Lang.KO to "쿠폰 사용이 완료되었습니다.\n%s캔이 지급되었습니다.",
Lang.EN to "Coupon redeemed successfully.\n%s cans have been granted.",
Lang.JA to "クーポンの使用が完了しました。\n%s缶が支給されました。"
),
"can.coupon.use_complete_point" to mapOf(
Lang.KO to "쿠폰 사용이 완료되었습니다.\n%s포인트가 지급되었습니다.",
Lang.EN to "Coupon redeemed successfully.\n%s points have been granted.",
Lang.JA to "クーポンの使用が完了しました。\n%sポイントが付与されました。"
),
"can.coupon.single_use_only" to mapOf(
Lang.KO to "해당 쿠폰은 1회만 충전이 가능합니다.",
Lang.EN to "This coupon can be used only once for charging.",
Lang.JA to "このクーポンは1回のみチャージに使用できます。"
),
"can.coupon.expired" to mapOf(
Lang.KO to "유효기간이 경과된 쿠폰입니다.",
Lang.EN to "This coupon has expired.",
Lang.JA to "有効期限が切れたクーポンです。"
),
"can.coupon.inactive" to mapOf(
Lang.KO to "이용이 불가능한 쿠폰입니다.",
Lang.EN to "This coupon is not available.",
Lang.JA to "利用できないクーポンです。"
),
"can.coupon.no_changes" to mapOf(
Lang.KO to "변경사항이 없습니다.",
Lang.EN to "No changes found.",
Lang.JA to "変更事項がありません。"
),
"can.coupon.validity_after_current" to mapOf(
Lang.KO to "유효기간은 기존 유효기간 이후 날짜로 설정하실 수 있습니다.",
Lang.EN to "Validity must be set after the current expiration date.",
Lang.JA to "有効期限は現在の有効期限以降に設定できます。"
),
"can.coupon.auth_required" to mapOf(
Lang.KO to "쿠폰은 본인인증을 하셔야 사용이 가능합니다.",
Lang.EN to "You must verify your identity to use coupons.",
Lang.JA to "クーポンの使用には本人認証が必要です。"
),
"can.coupon.download_failed_retry" to mapOf(
Lang.KO to "다운로드를 하지 못했습니다.\n다시 시도해 주세요.",
Lang.EN to "Download failed.\nPlease try again.",
Lang.JA to "ダウンロードできませんでした。\nもう一度お試しください。"
),
"can.coupon.download_filename" to mapOf(
Lang.KO to "쿠폰번호리스트.xlsx",
Lang.EN to "coupon_number_list.xlsx",
Lang.JA to "クーポン番号リスト.xlsx"
),
"can.coupon.download_header.index" to mapOf(
Lang.KO to "순번",
Lang.EN to "No.",
Lang.JA to "番号"
),
"can.coupon.download_header.number" to mapOf(
Lang.KO to "쿠폰번호",
Lang.EN to "Coupon Number",
Lang.JA to "クーポン番号"
),
"can.coupon.download_header.used" to mapOf(
Lang.KO to "사용여부",
Lang.EN to "Used",
Lang.JA to "使用有無"
),
"can.coupon.download_used_mark" to mapOf(
Lang.KO to "O",
Lang.EN to "O",
Lang.JA to "O"
),
"can.coupon.download_unused_mark" to mapOf(
Lang.KO to "X",
Lang.EN to "X",
Lang.JA to "X"
)
)
private val canPaymentMessages = mapOf(
"can.payment.invalid_request_retry" to mapOf(
Lang.KO to "잘못된 요청입니다.\n다시 시도해 주세요.",
Lang.EN to "Invalid request.\nPlease try again.",
Lang.JA to "不正なリクエストです。\nもう一度お試しください。"
),
"can.payment.invalid_reservation" to mapOf(
Lang.KO to "잘못된 예약정보 입니다.",
Lang.EN to "Invalid reservation information.",
Lang.JA to "無効な予約情報です。"
),
"can.payment.method.refund" to mapOf(
Lang.KO to "환불",
Lang.EN to "Refund",
Lang.JA to "返金"
),
"can.payment.insufficient_can" to mapOf(
Lang.KO to "%s 캔이 부족합니다. 충전 후 이용해 주세요.",
Lang.EN to "You are short of %s cans. Please recharge and try again.",
Lang.JA to "%s缶が不足しています。チャージしてからご利用ください。"
)
)
private val adminChatBannerMessages = mapOf(
"admin.chat.banner.image_save_failed" to mapOf(
Lang.KO to "이미지 저장에 실패했습니다.",
@@ -1546,6 +1718,10 @@ class SodaMessageSource {
auditionVoteMessages,
settlementRatioMessages,
adminCanMessages,
canChargeMessages,
canChargeEventMessages,
canCouponMessages,
canPaymentMessages,
adminChatBannerMessages,
adminChatCalculateMessages,
adminChatCharacterMessages,