다국어 메시지 분리 적용

This commit is contained in:
2025-12-23 16:19:04 +09:00
parent 39d13ab7c3
commit f429ffbbbe
7 changed files with 138 additions and 42 deletions

View File

@@ -78,7 +78,9 @@ class EventService(
startDateString: String, startDateString: String,
endDateString: String endDateString: String
): Long { ): Long {
if (detail == null && link.isNullOrBlank()) throw SodaException("상세이미지 혹은 링크를 등록하세요") if (detail == null && link.isNullOrBlank()) {
throw SodaException(messageKey = "event.detail_or_link_required")
}
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = LocalDate.parse(startDateString, dateTimeFormatter).atTime(0, 0) val startDate = LocalDate.parse(startDateString, dateTimeFormatter).atTime(0, 0)
@@ -146,7 +148,7 @@ class EventService(
event.detailImage = detailImagePath event.detailImage = detailImagePath
event.popupImage = popupImagePath event.popupImage = popupImagePath
return event.id ?: throw SodaException("이벤트 등록을 하지 못했습니다.") return event.id ?: throw SodaException(messageKey = "event.save_failed")
} }
@Transactional @Transactional
@@ -162,10 +164,10 @@ class EventService(
startDateString: String? = null, startDateString: String? = null,
endDateString: String? = null endDateString: String? = null
) { ) {
if (id <= 0) throw SodaException("잘못된 요청입니다.") if (id <= 0) throw SodaException(messageKey = "common.error.invalid_request")
val event = repository.findByIdOrNull(id) val event = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (thumbnail != null) { if (thumbnail != null) {
val metadata = ObjectMetadata() val metadata = ObjectMetadata()
@@ -234,9 +236,9 @@ class EventService(
@Transactional @Transactional
fun delete(id: Long) { fun delete(id: Long) {
if (id <= 0) throw SodaException("잘못된 요청입니다.") if (id <= 0) throw SodaException(messageKey = "common.error.invalid_request")
val event = repository.findByIdOrNull(id) val event = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
event.isActive = false event.isActive = false
} }

View File

@@ -12,12 +12,12 @@ class FaqService(
) { ) {
@Transactional @Transactional
fun save(request: CreateFaqRequest): Long { fun save(request: CreateFaqRequest): Long {
if (request.question.isBlank()) throw SodaException("질문을 입력하세요.") if (request.question.isBlank()) throw SodaException(messageKey = "faq.question_required")
if (request.answer.isBlank()) throw SodaException("답변을 입력하세요.") if (request.answer.isBlank()) throw SodaException(messageKey = "faq.answer_required")
if (request.category.isBlank()) throw SodaException("카테고리를 선택하세요.") if (request.category.isBlank()) throw SodaException(messageKey = "faq.category_required")
val category = queryRepository.getCategory(request.category) val category = queryRepository.getCategory(request.category)
?: throw SodaException("잘못된 카테고리 입니다.") ?: throw SodaException(messageKey = "faq.invalid_category")
val faq = Faq(request.question, request.answer) val faq = Faq(request.question, request.answer)
faq.category = category faq.category = category
@@ -28,30 +28,31 @@ class FaqService(
@Transactional @Transactional
fun modify(request: ModifyFaqRequest) { fun modify(request: ModifyFaqRequest) {
val faq = queryRepository.getFaq(request.id) val faq = queryRepository.getFaq(request.id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (request.question != null) { if (request.question != null) {
if (request.question.isBlank()) throw SodaException("질문을 입력하세요.") if (request.question.isBlank()) throw SodaException(messageKey = "faq.question_required")
faq.question = request.question faq.question = request.question
} }
if (request.answer != null) { if (request.answer != null) {
if (request.answer.isBlank()) throw SodaException("답변을 입력하세요.") if (request.answer.isBlank()) throw SodaException(messageKey = "faq.answer_required")
faq.answer = request.answer faq.answer = request.answer
} }
if (request.category != null) { if (request.category != null) {
if (request.category.isBlank()) throw SodaException("카테고리를 선택하세요.") if (request.category.isBlank()) throw SodaException(messageKey = "faq.category_required")
val category = queryRepository.getCategory(request.category) ?: throw SodaException("잘못된 카테고리 입니다.") val category = queryRepository.getCategory(request.category)
?: throw SodaException(messageKey = "faq.invalid_category")
faq.category = category faq.category = category
} }
} }
@Transactional @Transactional
fun delete(id: Long) { fun delete(id: Long) {
if (id <= 0) throw SodaException("잘못된 요청입니다.") if (id <= 0) throw SodaException(messageKey = "common.error.invalid_request")
val faq = repository.findByIdOrNull(id) val faq = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
faq.isActive = false faq.isActive = false
} }

View File

@@ -626,6 +626,75 @@ class SodaMessageSource {
) )
) )
private val eventMessages = mapOf(
"event.detail_or_link_required" to mapOf(
Lang.KO to "상세이미지 혹은 링크를 등록하세요",
Lang.EN to "Please register a detail image or link.",
Lang.JA to "詳細画像またはリンクを登録してください。"
),
"event.save_failed" to mapOf(
Lang.KO to "이벤트 등록을 하지 못했습니다.",
Lang.EN to "Failed to register the event.",
Lang.JA to "イベントの登録に失敗しました。"
)
)
private val faqMessages = mapOf(
"faq.question_required" to mapOf(
Lang.KO to "질문을 입력하세요.",
Lang.EN to "Please enter a question.",
Lang.JA to "質問を入力してください。"
),
"faq.answer_required" to mapOf(
Lang.KO to "답변을 입력하세요.",
Lang.EN to "Please enter an answer.",
Lang.JA to "回答を入力してください。"
),
"faq.category_required" to mapOf(
Lang.KO to "카테고리를 선택하세요.",
Lang.EN to "Please select a category.",
Lang.JA to "カテゴリーを選択してください。"
),
"faq.invalid_category" to mapOf(
Lang.KO to "잘못된 카테고리 입니다.",
Lang.EN to "Invalid category.",
Lang.JA to "不正なカテゴリーです。"
)
)
private val liveReservationMessages = mapOf(
"live.reservation.invalid_request_retry" to mapOf(
Lang.KO to "잘못된 요청입니다.\n다시 시도해 주세요.",
Lang.EN to "Invalid request.\nPlease try again.",
Lang.JA to "不正なリクエストです。\nもう一度やり直してください。"
),
"live.reservation.already_reserved" to mapOf(
Lang.KO to "이미 예약한 라이브 입니다.",
Lang.EN to "You have already reserved this live.",
Lang.JA to "すでに予約済みのライブです。"
),
"live.reservation.invalid_reservation" to mapOf(
Lang.KO to "잘못된 예약정보 입니다.",
Lang.EN to "Invalid reservation information.",
Lang.JA to "不正な予約情報です。"
),
"live.reservation.cancel_not_allowed_within_4_hours" to mapOf(
Lang.KO to "라이브 시작 4시간 이내에는 예약취소가 불가능 합니다.",
Lang.EN to "Reservations cannot be canceled within 4 hours of the live start.",
Lang.JA to "ライブ開始4時間以内は予約をキャンセルできません。"
),
"live.reservation.price_free" to mapOf(
Lang.KO to "무료",
Lang.EN to "Free",
Lang.JA to "無料"
),
"live.reservation.datetime_format" to mapOf(
Lang.KO to "yyyy년 M월 d일 (E), a hh:mm",
Lang.EN to "yyyy MMM d (EEE), h:mm a",
Lang.JA to "yyyy年 M月 d日 (E) a hh:mm"
)
)
private val liveRouletteMessages = mapOf( private val liveRouletteMessages = mapOf(
"live.roulette.unavailable" to mapOf( "live.roulette.unavailable" to mapOf(
Lang.KO to "룰렛을 사용할 수 없습니다.", Lang.KO to "룰렛을 사용할 수 없습니다.",
@@ -949,7 +1018,10 @@ class SodaMessageSource {
memberMessages, memberMessages,
memberValidationMessages, memberValidationMessages,
memberSocialMessages, memberSocialMessages,
eventMessages,
faqMessages,
liveRouletteMessages, liveRouletteMessages,
liveReservationMessages,
liveTagMessages, liveTagMessages,
liveRoomMessages, liveRoomMessages,
liveRoomMenuMessages, liveRoomMenuMessages,

View File

@@ -85,12 +85,14 @@ class TokenProvider(
val authorities = claims[AUTHORITIES_KEY].toString().split(",").map { SimpleGrantedAuthority(it) } val authorities = claims[AUTHORITIES_KEY].toString().split(",").map { SimpleGrantedAuthority(it) }
val memberToken = tokenRepository.findByIdOrNull(id = claims.subject.toLong()) val memberToken = tokenRepository.findByIdOrNull(id = claims.subject.toLong())
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
if (!memberToken.tokenSet.contains(token)) throw SodaException("로그인 정보를 확인해주세요.") if (!memberToken.tokenSet.contains(token)) {
throw SodaException(messageKey = "common.error.bad_credentials")
}
val member = repository.findByIdOrNull(id = claims.subject.toLong()) val member = repository.findByIdOrNull(id = claims.subject.toLong())
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
val principal = MemberAdapter(member) val principal = MemberAdapter(member)
return UsernamePasswordAuthenticationToken(principal, token, authorities) return UsernamePasswordAuthenticationToken(principal, token, authorities)

View File

@@ -30,7 +30,7 @@ class LiveRecommendController(private val service: LiveRecommendService) {
fun getFollowingChannelList( fun getFollowingChannelList(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.getFollowingChannelList(member)) ApiResponse.ok(service.getFollowingChannelList(member))
} }
@@ -40,7 +40,7 @@ class LiveRecommendController(private val service: LiveRecommendService) {
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable pageable: Pageable
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.getFollowingAllChannelList(member, pageable)) ApiResponse.ok(service.getFollowingAllChannelList(member, pageable))
} }

View File

@@ -21,7 +21,7 @@ class LiveReservationController(private val service: LiveReservationService) {
@RequestBody request: MakeLiveReservationRequest, @RequestBody request: MakeLiveReservationRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.makeReservation(request, member.id!!)) ApiResponse.ok(service.makeReservation(request, member.id!!))
} }
@@ -32,7 +32,7 @@ class LiveReservationController(private val service: LiveReservationService) {
@RequestParam(value = "timezone") timezone: String, @RequestParam(value = "timezone") timezone: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.getReservationList(member.id!!, isActive, timezone)) ApiResponse.ok(service.getReservationList(member.id!!, isActive, timezone))
} }
@@ -42,7 +42,7 @@ class LiveReservationController(private val service: LiveReservationService) {
@RequestParam(value = "timezone") timezone: String, @RequestParam(value = "timezone") timezone: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.getReservation(id, member.id!!, timezone)) ApiResponse.ok(service.getReservation(id, member.id!!, timezone))
} }
@@ -51,7 +51,7 @@ class LiveReservationController(private val service: LiveReservationService) {
@RequestBody request: CancelLiveReservationRequest, @RequestBody request: CancelLiveReservationRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.cancelReservation(request, member.id!!)) ApiResponse.ok(service.cancelReservation(request, member.id!!))
} }
} }

View File

@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.live.reservation
import kr.co.vividnext.sodalive.can.payment.CanPaymentService import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.common.SodaException 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.live.room.LiveRoomRepository import kr.co.vividnext.sodalive.live.room.LiveRoomRepository
import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
@@ -21,32 +23,36 @@ class LiveReservationService(
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val canPaymentService: CanPaymentService, private val canPaymentService: CanPaymentService,
private val liveReservationCancelRepository: LiveReservationCancelRepository, private val liveReservationCancelRepository: LiveReservationCancelRepository,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String private val cloudFrontHost: String
) { ) {
fun makeReservation(request: MakeLiveReservationRequest, memberId: Long): MakeLiveReservationResponse { fun makeReservation(request: MakeLiveReservationRequest, memberId: Long): MakeLiveReservationResponse {
val room = liveRoomRepository.findByIdOrNull(id = request.roomId) val room = liveRoomRepository.findByIdOrNull(id = request.roomId)
?: throw SodaException(message = "잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "live.reservation.invalid_request_retry")
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException(message = "로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
if ( if (
room.member!!.id!! != memberId && room.member!!.id!! != memberId &&
room.type == LiveRoomType.PRIVATE && room.type == LiveRoomType.PRIVATE &&
(request.password == null || request.password != room.password) (request.password == null || request.password != room.password)
) { ) {
throw SodaException("비밀번호가 일치하지 않습니다.\n다시 확인 후 입력해주세요.") throw SodaException(messageKey = "live.room.password_mismatch")
} }
if (repository.isExistsReservation(roomId = request.roomId, memberId = memberId)) { if (repository.isExistsReservation(roomId = request.roomId, memberId = memberId)) {
throw SodaException("이미 예약한 라이브 입니다.") throw SodaException(messageKey = "live.reservation.already_reserved")
} }
val haveCan = member.getChargeCan(request.container) + member.getRewardCan(request.container) val haveCan = member.getChargeCan(request.container) + member.getRewardCan(request.container)
if (haveCan < room.price) { if (haveCan < room.price) {
throw SodaException("${room.price - haveCan}캔이 부족합니다. 충전 후 이용해 주세요.") val messageTemplate = messageSource.getMessage("live.room.insufficient_can", langContext.lang).orEmpty()
val message = String.format(messageTemplate, room.price - haveCan)
throw SodaException(message = message)
} }
if (room.price > 0) { if (room.price > 0) {
@@ -67,16 +73,21 @@ class LiveReservationService(
val beginDateTime = room.beginDateTime val beginDateTime = room.beginDateTime
.atZone(ZoneId.of("UTC")) .atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(request.timezone)) .withZoneSameInstant(ZoneId.of(request.timezone))
val reservationDateFormat = messageSource.getMessage(
"live.reservation.datetime_format",
langContext.lang
).orEmpty()
return MakeLiveReservationResponse( return MakeLiveReservationResponse(
reservationId = reservation.id!!, reservationId = reservation.id!!,
nickname = room.member!!.nickname, nickname = room.member!!.nickname,
title = room.title, title = room.title,
beginDateString = beginDateTime.format(DateTimeFormatter.ofPattern("yyyy년 M월 d일 (E), a hh:mm")), beginDateString = beginDateTime.format(DateTimeFormatter.ofPattern(reservationDateFormat)),
price = if (room.price > 0) { price = if (room.price > 0) {
"${room.price}" val priceTemplate = messageSource.getMessage("live.room.can_title", langContext.lang).orEmpty()
String.format(priceTemplate, room.price)
} else { } else {
"무료" messageSource.getMessage("live.reservation.price_free", langContext.lang).orEmpty()
}, },
haveCan = haveCan, haveCan = haveCan,
useCan = room.price, useCan = room.price,
@@ -85,6 +96,10 @@ class LiveReservationService(
} }
fun getReservationList(memberId: Long, active: Boolean, timezone: String): List<GetLiveReservationResponse> { fun getReservationList(memberId: Long, active: Boolean, timezone: String): List<GetLiveReservationResponse> {
val detailDateFormat = messageSource.getMessage(
"live.room.datetime_format_detail",
langContext.lang
).orEmpty()
return repository return repository
.getReservationListByMemberId(memberId, active) .getReservationListByMemberId(memberId, active)
.asSequence() .asSequence()
@@ -105,7 +120,7 @@ class LiveReservationService(
price = it.room!!.price, price = it.room!!.price,
masterNickname = it.room!!.member!!.nickname, masterNickname = it.room!!.member!!.nickname,
beginDateTime = beginDateTime.format( beginDateTime = beginDateTime.format(
DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") DateTimeFormatter.ofPattern(detailDateFormat)
), ),
cancelable = beginDateTime.minusHours(4).isAfter( cancelable = beginDateTime.minusHours(4).isAfter(
LocalDateTime.now() LocalDateTime.now()
@@ -119,8 +134,12 @@ class LiveReservationService(
fun getReservation(reservationId: Long, memberId: Long, timezone: String): GetLiveReservationResponse { fun getReservation(reservationId: Long, memberId: Long, timezone: String): GetLiveReservationResponse {
val reservation = repository.getReservationByReservationAndMemberId(reservationId, memberId) val reservation = repository.getReservationByReservationAndMemberId(reservationId, memberId)
?: throw SodaException("잘못된 예약정보 입니다.") ?: throw SodaException(messageKey = "live.reservation.invalid_reservation")
val detailDateFormat = messageSource.getMessage(
"live.room.datetime_format_detail",
langContext.lang
).orEmpty()
val beginDateTime = reservation.room!!.beginDateTime val beginDateTime = reservation.room!!.beginDateTime
.atZone(ZoneId.of("UTC")) .atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(timezone)) .withZoneSameInstant(ZoneId.of(timezone))
@@ -137,7 +156,7 @@ class LiveReservationService(
price = reservation.room!!.price, price = reservation.room!!.price,
masterNickname = reservation.room!!.member!!.nickname, masterNickname = reservation.room!!.member!!.nickname,
beginDateTime = beginDateTime.format( beginDateTime = beginDateTime.format(
DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") DateTimeFormatter.ofPattern(detailDateFormat)
), ),
cancelable = beginDateTime.minusHours(4).isAfter( cancelable = beginDateTime.minusHours(4).isAfter(
LocalDateTime.now() LocalDateTime.now()
@@ -150,22 +169,22 @@ class LiveReservationService(
@Transactional @Transactional
fun cancelReservation(request: CancelLiveReservationRequest, memberId: Long) { fun cancelReservation(request: CancelLiveReservationRequest, memberId: Long) {
if (request.reason.isBlank()) { if (request.reason.isBlank()) {
throw SodaException("취소사유를 입력하세요.") throw SodaException(messageKey = "live.room.cancel_reason_required")
} }
val reservation = repository.findByIdOrNull(request.reservationId) val reservation = repository.findByIdOrNull(request.reservationId)
?: throw SodaException("잘못된 예약정보 입니다.") ?: throw SodaException(messageKey = "live.reservation.invalid_reservation")
if (reservation.member == null || reservation.member!!.id!! != memberId) { if (reservation.member == null || reservation.member!!.id!! != memberId) {
throw SodaException("잘못된 예약정보 입니다.") throw SodaException(messageKey = "live.reservation.invalid_reservation")
} }
if (reservation.room == null || reservation.room?.id == null) { if (reservation.room == null || reservation.room?.id == null) {
throw SodaException("잘못된 예약정보 입니다.") throw SodaException(messageKey = "live.reservation.invalid_reservation")
} }
if (reservation.room!!.beginDateTime.isBefore(LocalDateTime.now().plusHours(4))) { if (reservation.room!!.beginDateTime.isBefore(LocalDateTime.now().plusHours(4))) {
throw SodaException("라이브 시작 4시간 이내에는 예약취소가 불가능 합니다.") throw SodaException(messageKey = "live.reservation.cancel_not_allowed_within_4_hours")
} }
if (reservation.room!!.price > 0) { if (reservation.room!!.price > 0) {