캔 결제 메시지 다국어 처리

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

@@ -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)
}
}