Compare commits

...

19 Commits

Author SHA1 Message Date
60e654cda9 클라이언트 메시지 다국어 처리 2025-12-23 19:22:06 +09:00
e987a56544 콘텐츠 메시지 다국어 처리 2025-12-23 19:03:38 +09:00
9d619450ef 채팅 메시지 다국어 분리 2025-12-23 18:38:54 +09:00
6e8a88178c 캔 결제 메시지 다국어 처리 2025-12-23 18:09:17 +09:00
58f7a8654b 알림/오디션 메시지 다국어 분리
알림/오디션 오류 응답 메시지를 키 기반 다국어로 분리
2025-12-23 17:45:47 +09:00
7ef654e89d 관리자 메시지 다국어 처리 2025-12-23 17:35:38 +09:00
291f9a265b 크리에이터 시리즈 메시지 다국어 처리
크리에이터 정산/시리즈 API 응답 메시지를 다국어 키로 제공한다.
2025-12-23 17:09:59 +09:00
f38382d2be 크리에이터 관리자 메시지 다국어화 2025-12-23 16:54:48 +09:00
4087d11420 탐색 커뮤니티 다국어 메시지 분리 2025-12-23 16:44:27 +09:00
f429ffbbbe 다국어 메시지 분리 적용 2025-12-23 16:19:04 +09:00
39d13ab7c3 라이브룸 메시지 다국어 처리 2025-12-23 14:20:52 +09:00
fd94df338b 라이브 룰렛 태그 메시지 다국어 처리 2025-12-23 13:52:53 +09:00
67b909daed 회원 메시지 다국어 처리
회원/인증 API 응답 메시지를 다국어 키로 분리함.
2025-12-23 13:26:15 +09:00
4dcf9f6ed1 클라이언트 메시지 다국어 처리
공개 API 변경 없음.
2025-12-22 23:12:29 +09:00
93e0411337 관리자 콘텐츠 메시지 다국어 처리 2025-12-22 22:51:19 +09:00
280b21c3cb 관리자 채팅 메시지 다국어 처리 2025-12-22 22:30:05 +09:00
14d0ae9851 오디션 배역 등 메시지 다국어 처리 2025-12-22 21:49:07 +09:00
8785741855 오디션 메시지 다국어 처리 2025-12-22 18:54:10 +09:00
ff1b8aa413 예외 메시지 다국어 처리를 위한 키 기반 구조 도입 2025-12-22 16:39:06 +09:00
148 changed files with 3886 additions and 1096 deletions

View File

@@ -7,6 +7,8 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType 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.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
@@ -22,6 +24,8 @@ class AdminAuditionService(
private val repository: AdminAuditionRepository, private val repository: AdminAuditionRepository,
private val roleRepository: AdminAuditionRoleRepository, private val roleRepository: AdminAuditionRoleRepository,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val langContext: LangContext,
private val messageSource: SodaMessageSource,
@Value("\${cloud.aws.s3.bucket}") @Value("\${cloud.aws.s3.bucket}")
private val bucket: String private val bucket: String
@@ -44,7 +48,7 @@ class AdminAuditionService(
fun updateAudition(image: MultipartFile?, requestString: String) { fun updateAudition(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateAuditionRequest::class.java) val request = objectMapper.readValue(requestString, UpdateAuditionRequest::class.java)
val audition = repository.findByIdOrNull(id = request.id) val audition = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "admin.audition.invalid_request_retry")
if (request.title != null) { if (request.title != null) {
audition.title = request.title audition.title = request.title
@@ -63,7 +67,7 @@ class AdminAuditionService(
(audition.status == AuditionStatus.COMPLETED || audition.status == AuditionStatus.IN_PROGRESS) && (audition.status == AuditionStatus.COMPLETED || audition.status == AuditionStatus.IN_PROGRESS) &&
request.status == AuditionStatus.NOT_STARTED request.status == AuditionStatus.NOT_STARTED
) { ) {
throw SodaException("모집전 상태로 변경할 수 없습니다.") throw SodaException(messageKey = "admin.audition.status_cannot_revert")
} }
audition.status = request.status audition.status = request.status
@@ -88,11 +92,14 @@ class AdminAuditionService(
} }
if (request.status != null && request.status == AuditionStatus.IN_PROGRESS && audition.isActive) { if (request.status != null && request.status == AuditionStatus.IN_PROGRESS && audition.isActive) {
val title = messageSource.getMessage("admin.audition.fcm.title.new", langContext.lang).orEmpty()
val messageTemplate = messageSource.getMessage("admin.audition.fcm.message.new", langContext.lang).orEmpty()
val message = String.format(messageTemplate, audition.title)
applicationEventPublisher.publishEvent( applicationEventPublisher.publishEvent(
FcmEvent( FcmEvent(
type = FcmEventType.IN_PROGRESS_AUDITION, type = FcmEventType.IN_PROGRESS_AUDITION,
title = "새로운 오디션 등록!", title = title,
message = "'${audition.title}'이 등록되었습니다. 지금 바로 오리지널 오디오 드라마 오디션에 지원해보세요!", message = message,
isAuth = audition.isAdult, isAuth = audition.isAdult,
auditionId = audition.id ?: -1 auditionId = audition.id ?: -1
) )

View File

@@ -11,11 +11,11 @@ data class CreateAuditionRequest(
) { ) {
init { init {
if (title.isBlank()) { if (title.isBlank()) {
throw SodaException("오디션 제목을 입력하세요") throw SodaException(messageKey = "admin.audition.title_required")
} }
if (information.isBlank() || information.length < 10) { if (information.isBlank() || information.length < 10) {
throw SodaException("오디션 정보는 최소 10글자 입니다") throw SodaException(messageKey = "admin.audition.information_min_length")
} }
} }

View File

@@ -10,7 +10,7 @@ class AdminAuditionApplicantService(private val repository: AdminAuditionApplica
@Transactional @Transactional
fun deleteAuditionApplicant(id: Long) { fun deleteAuditionApplicant(id: Long) {
val applicant = repository.findByIdOrNull(id) val applicant = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
applicant.isActive = false applicant.isActive = false
} }

View File

@@ -31,7 +31,7 @@ class AdminAuditionRoleService(
auditionScriptUrl = request.auditionScriptUrl auditionScriptUrl = request.auditionScriptUrl
) )
val audition = auditionRepository.findByIdOrNull(id = request.auditionId) val audition = auditionRepository.findByIdOrNull(id = request.auditionId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "admin.audition.invalid_request_retry")
auditionRole.audition = audition auditionRole.audition = audition
repository.save(auditionRole) repository.save(auditionRole)
@@ -48,15 +48,19 @@ class AdminAuditionRoleService(
fun updateAuditionRole(image: MultipartFile?, requestString: String) { fun updateAuditionRole(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateAuditionRoleRequest::class.java) val request = objectMapper.readValue(requestString, UpdateAuditionRoleRequest::class.java)
val auditionRole = repository.findByIdOrNull(id = request.id) val auditionRole = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "admin.audition.invalid_request_retry")
if (!request.name.isNullOrBlank()) { if (!request.name.isNullOrBlank()) {
if (request.name.length < 2) throw SodaException("배역 이름은 최소 2글자 입니다") if (request.name.length < 2) {
throw SodaException(messageKey = "admin.audition.role.name_min_length")
}
auditionRole.name = request.name auditionRole.name = request.name
} }
if (!request.information.isNullOrBlank()) { if (!request.information.isNullOrBlank()) {
if (request.information.length < 10) throw SodaException("오디션 배역 정보는 최소 10글자 입니다") if (request.information.length < 10) {
throw SodaException(messageKey = "admin.audition.role.information_min_length")
}
auditionRole.information = request.information auditionRole.information = request.information
} }

View File

@@ -10,19 +10,19 @@ data class CreateAuditionRoleRequest(
) { ) {
init { init {
if (auditionId < 0) { if (auditionId < 0) {
throw SodaException("캐릭터가 등록될 오디션을 선택하세요") throw SodaException(messageKey = "admin.audition.role.audition_required")
} }
if (name.isBlank() || name.length < 2) { if (name.isBlank() || name.length < 2) {
throw SodaException("캐릭터명을 입력하세요") throw SodaException(messageKey = "admin.audition.role.name_required")
} }
if (auditionScriptUrl.isBlank() || auditionScriptUrl.length < 10) { if (auditionScriptUrl.isBlank() || auditionScriptUrl.length < 10) {
throw SodaException("오디션 대본 URL을 입력하세요") throw SodaException(messageKey = "admin.audition.role.script_url_required")
} }
if (information.isBlank() || information.length < 10) { if (information.isBlank() || information.length < 10) {
throw SodaException("오디션 캐릭터 정보는 최소 10글자 입니다") throw SodaException(messageKey = "admin.audition.role.information_required")
} }
} }
} }

View File

@@ -13,7 +13,7 @@ data class UpdateAuditionRoleRequest(
) { ) {
init { init {
if (id < 0) { if (id < 0) {
throw SodaException("잘못된 요청입니다.") throw SodaException(messageKey = "common.error.invalid_request")
} }
} }
} }

View File

@@ -15,10 +15,10 @@ class CreatorSettlementRatioService(
@Transactional @Transactional
fun createCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) { fun createCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
val creator = memberRepository.findByIdOrNull(request.memberId) val creator = memberRepository.findByIdOrNull(request.memberId)
?: throw SodaException("잘못된 크리에이터 입니다.") ?: throw SodaException(messageKey = "admin.settlement_ratio.invalid_creator")
if (creator.role != MemberRole.CREATOR) { if (creator.role != MemberRole.CREATOR) {
throw SodaException("잘못된 크리에이터 입니다.") throw SodaException(messageKey = "admin.settlement_ratio.invalid_creator")
} }
val existing = repository.findByMemberId(request.memberId) val existing = repository.findByMemberId(request.memberId)
@@ -43,12 +43,12 @@ class CreatorSettlementRatioService(
@Transactional @Transactional
fun updateCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) { fun updateCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
val creator = memberRepository.findByIdOrNull(request.memberId) val creator = memberRepository.findByIdOrNull(request.memberId)
?: throw SodaException("잘못된 크리에이터 입니다.") ?: throw SodaException(messageKey = "admin.settlement_ratio.invalid_creator")
if (creator.role != MemberRole.CREATOR) { if (creator.role != MemberRole.CREATOR) {
throw SodaException("잘못된 크리에이터 입니다.") throw SodaException(messageKey = "admin.settlement_ratio.invalid_creator")
} }
val existing = repository.findByMemberId(request.memberId) val existing = repository.findByMemberId(request.memberId)
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.") ?: throw SodaException(messageKey = "admin.settlement_ratio.not_found")
existing.restore() existing.restore()
existing.updateValues( existing.updateValues(
request.subsidy, request.subsidy,
@@ -62,7 +62,7 @@ class CreatorSettlementRatioService(
@Transactional @Transactional
fun deleteCreatorSettlementRatio(memberId: Long) { fun deleteCreatorSettlementRatio(memberId: Long) {
val existing = repository.findByMemberId(memberId) val existing = repository.findByMemberId(memberId)
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.") ?: throw SodaException(messageKey = "admin.settlement_ratio.not_found")
existing.softDelete() existing.softDelete()
repository.save(existing) repository.save(existing)
} }

View File

@@ -33,21 +33,21 @@ class AdminCanService(
@Transactional @Transactional
fun deleteCan(id: Long) { fun deleteCan(id: Long) {
val can = repository.findByIdOrNull(id) val can = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
can.status = CanStatus.END_OF_SALE can.status = CanStatus.END_OF_SALE
} }
@Transactional @Transactional
fun charge(request: AdminCanChargeRequest) { fun charge(request: AdminCanChargeRequest) {
if (request.can <= 0) throw SodaException("1 캔 이상 입력하세요.") if (request.can <= 0) throw SodaException(messageKey = "admin.can.min_amount")
if (request.method.isBlank()) throw SodaException("기록내용을 입력하세요.") if (request.method.isBlank()) throw SodaException(messageKey = "admin.can.method_required")
val ids = request.memberIds.distinct() val ids = request.memberIds.distinct()
if (ids.isEmpty()) throw SodaException("회원번호를 입력하세요.") if (ids.isEmpty()) throw SodaException(messageKey = "admin.can.member_ids_required")
val members = memberRepository.findAllById(ids).toList() val members = memberRepository.findAllById(ids).toList()
if (members.size != ids.size) throw SodaException("잘못된 회원번호 입니다.") if (members.size != ids.size) throw SodaException(messageKey = "admin.can.invalid_member_ids")
members.forEach { member -> members.forEach { member ->
val charge = Charge(0, request.can, status = ChargeStatus.ADMIN) val charge = Charge(0, request.can, status = ChargeStatus.ADMIN)

View File

@@ -13,6 +13,8 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
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.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
@@ -35,6 +37,8 @@ class AdminChatBannerController(
private val bannerService: ChatCharacterBannerService, private val bannerService: ChatCharacterBannerService,
private val adminCharacterService: AdminChatCharacterService, private val adminCharacterService: AdminChatCharacterService,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val langContext: LangContext,
private val messageSource: SodaMessageSource,
@Value("\${cloud.aws.s3.bucket}") @Value("\${cloud.aws.s3.bucket}")
private val s3Bucket: String, private val s3Bucket: String,
@@ -158,8 +162,8 @@ class AdminChatBannerController(
filePath = "characters/banners/$bannerId/$fileName", filePath = "characters/banners/$bannerId/$fileName",
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (_: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.banner.image_save_failed")
} }
} }
@@ -208,7 +212,8 @@ class AdminChatBannerController(
fun deleteBanner(@PathVariable bannerId: Long) = run { fun deleteBanner(@PathVariable bannerId: Long) = run {
bannerService.deleteBanner(bannerId) bannerService.deleteBanner(bannerId)
ApiResponse.ok("배너가 성공적으로 삭제되었습니다.") val message = messageSource.getMessage("admin.chat.banner.delete_success", langContext.lang)
ApiResponse.ok(message)
} }
/** /**
@@ -224,6 +229,7 @@ class AdminChatBannerController(
) = run { ) = run {
bannerService.updateBannerOrders(request.ids) bannerService.updateBannerOrders(request.ids)
ApiResponse.ok(null, "배너 정렬 순서가 성공적으로 변경되었습니다.") val message = messageSource.getMessage("admin.chat.banner.reorder_success", langContext.lang)
ApiResponse.ok(null, message)
} }
} }

View File

@@ -29,13 +29,13 @@ class AdminChatCalculateService(
val todayKst = LocalDate.now(kstZone) val todayKst = LocalDate.now(kstZone)
if (endDate.isAfter(todayKst)) { if (endDate.isAfter(todayKst)) {
throw SodaException("끝 날짜는 오늘 날짜까지만 입력 가능합니다.") throw SodaException(messageKey = "admin.chat.calculate.end_date_max_today")
} }
if (startDate.isAfter(endDate)) { if (startDate.isAfter(endDate)) {
throw SodaException("시작 날짜는 끝 날짜보다 이후일 수 없습니다.") throw SodaException(messageKey = "admin.chat.calculate.start_date_after_end")
} }
if (endDate.isAfter(startDate.plusMonths(6))) { if (endDate.isAfter(startDate.plusMonths(6))) {
throw SodaException("조회 가능 기간은 최대 6개월입니다.") throw SodaException(messageKey = "admin.chat.calculate.max_period_6_months")
} }
val startUtc = startDateStr.convertLocalDateTime() val startUtc = startDateStr.convertLocalDateTime()

View File

@@ -124,7 +124,7 @@ class AdminChatCharacterController(
// 외부 API 호출 전 DB에 동일한 이름이 있는지 조회 // 외부 API 호출 전 DB에 동일한 이름이 있는지 조회
val existingCharacter = service.findByName(request.name) val existingCharacter = service.findByName(request.name)
if (existingCharacter != null) { if (existingCharacter != null) {
throw SodaException("동일한 이름은 등록이 불가능합니다: ${request.name}") throw SodaException(messageKey = "admin.chat.character.duplicate_name")
} }
// 1. 외부 API 호출 // 1. 외부 API 호출
@@ -233,14 +233,18 @@ class AdminChatCharacterController(
// success가 false이면 throw // success가 false이면 throw
if (!apiResponse.success) { if (!apiResponse.success) {
throw SodaException(apiResponse.message ?: "등록에 실패했습니다. 다시 시도해 주세요.") val apiMessage = apiResponse.message
if (apiMessage.isNullOrBlank()) {
throw SodaException(messageKey = "admin.chat.character.register_failed_retry")
}
throw SodaException(apiMessage)
} }
// success가 true이면 data.id 반환 // success가 true이면 data.id 반환
return apiResponse.data?.id ?: throw SodaException("등록에 실패했습니다. 응답에 ID가 없습니다.") return apiResponse.data?.id ?: throw SodaException(messageKey = "admin.chat.character.register_failed_no_id")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
throw SodaException("${e.message}, 등록에 실패했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "admin.chat.character.register_failed_retry")
} }
} }
@@ -257,7 +261,7 @@ class AdminChatCharacterController(
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.character.image_save_failed")
} }
} }
@@ -297,19 +301,19 @@ class AdminChatCharacterController(
request.originalWorkId != null request.originalWorkId != null
if (!hasChangedData && !hasImage && !hasDbOnlyChanges) { if (!hasChangedData && !hasImage && !hasDbOnlyChanges) {
throw SodaException("변경된 데이터가 없습니다.") throw SodaException(messageKey = "admin.chat.character.no_changes")
} }
// 외부 API로 전달할 변경이 있을 때만 외부 API 호출(3가지 필드만 변경된 경우는 호출하지 않음) // 외부 API로 전달할 변경이 있을 때만 외부 API 호출(3가지 필드만 변경된 경우는 호출하지 않음)
if (hasChangedData) { if (hasChangedData) {
val chatCharacter = service.findById(request.id) val chatCharacter = service.findById(request.id)
?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}") ?: throw SodaException(messageKey = "admin.chat.character.not_found")
// 이름이 수정된 경우 DB에 동일한 이름이 있는지 확인 // 이름이 수정된 경우 DB에 동일한 이름이 있는지 확인
if (request.name != null && request.name != chatCharacter.name) { if (request.name != null && request.name != chatCharacter.name) {
val existingCharacter = service.findByName(request.name) val existingCharacter = service.findByName(request.name)
if (existingCharacter != null) { if (existingCharacter != null) {
throw SodaException("동일한 이름은 등록이 불가능합니다: ${request.name}") throw SodaException(messageKey = "admin.chat.character.duplicate_name")
} }
} }
@@ -438,11 +442,15 @@ class AdminChatCharacterController(
// success가 false이면 throw // success가 false이면 throw
if (!apiResponse.success) { if (!apiResponse.success) {
throw SodaException(apiResponse.message ?: "수정에 실패했습니다. 다시 시도해 주세요.") val apiMessage = apiResponse.message
if (apiMessage.isNullOrBlank()) {
throw SodaException(messageKey = "admin.chat.character.update_failed_retry")
}
throw SodaException(apiMessage)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
throw SodaException("${e.message} 수정에 실패했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "admin.chat.character.update_failed_retry")
} }
} }
} }

View File

@@ -63,7 +63,7 @@ class CharacterCurationAdminController(
@RequestBody request: CharacterCurationAddCharacterRequest @RequestBody request: CharacterCurationAddCharacterRequest
): ApiResponse<Boolean> { ): ApiResponse<Boolean> {
val ids = request.characterIds.filter { it > 0 }.distinct() val ids = request.characterIds.filter { it > 0 }.distinct()
if (ids.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다") if (ids.isEmpty()) throw SodaException(messageKey = "admin.chat.curation.character_ids_empty")
service.addCharacters(curationId, ids) service.addCharacters(curationId, ids)
return ApiResponse.ok(true) return ApiResponse.ok(true)
} }

View File

@@ -32,7 +32,7 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun update(request: CharacterCurationUpdateRequest): CharacterCuration { fun update(request: CharacterCurationUpdateRequest): CharacterCuration {
val curation = curationRepository.findById(request.id) val curation = curationRepository.findById(request.id)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: ${request.id}") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
request.title?.let { curation.title = it } request.title?.let { curation.title = it }
request.isAdult?.let { curation.isAdult = it } request.isAdult?.let { curation.isAdult = it }
@@ -44,7 +44,7 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun softDelete(curationId: Long) { fun softDelete(curationId: Long) {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
curation.isActive = false curation.isActive = false
curationRepository.save(curation) curationRepository.save(curation)
} }
@@ -53,7 +53,7 @@ class CharacterCurationAdminService(
fun reorder(ids: List<Long>) { fun reorder(ids: List<Long>) {
ids.forEachIndexed { index, id -> ids.forEachIndexed { index, id ->
val curation = curationRepository.findById(id) val curation = curationRepository.findById(id)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $id") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
curation.sortOrder = index + 1 curation.sortOrder = index + 1
curationRepository.save(curation) curationRepository.save(curation)
} }
@@ -61,14 +61,14 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun addCharacters(curationId: Long, characterIds: List<Long>) { fun addCharacters(curationId: Long, characterIds: List<Long>) {
if (characterIds.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다") if (characterIds.isEmpty()) throw SodaException(messageKey = "admin.chat.curation.character_ids_empty")
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
if (!curation.isActive) throw SodaException("비활성화된 큐레이션입니다: $curationId") if (!curation.isActive) throw SodaException(messageKey = "admin.chat.curation.inactive")
val uniqueIds = characterIds.filter { it > 0 }.distinct() val uniqueIds = characterIds.filter { it > 0 }.distinct()
if (uniqueIds.isEmpty()) throw SodaException("유효한 캐릭터 ID가 없습니다") if (uniqueIds.isEmpty()) throw SodaException(messageKey = "admin.chat.curation.invalid_character_ids")
// 활성 캐릭터만 조회 (조회 단계에서 검증 포함) // 활성 캐릭터만 조회 (조회 단계에서 검증 포함)
val characters = characterRepository.findByIdInAndIsActiveTrue(uniqueIds) val characters = characterRepository.findByIdInAndIsActiveTrue(uniqueIds)
@@ -101,23 +101,23 @@ class CharacterCurationAdminService(
@Transactional @Transactional
fun removeCharacter(curationId: Long, characterId: Long) { fun removeCharacter(curationId: Long, characterId: Long) {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
val mappings = mappingRepository.findByCuration(curation) val mappings = mappingRepository.findByCuration(curation)
val target = mappings.firstOrNull { it.chatCharacter.id == characterId } val target = mappings.firstOrNull { it.chatCharacter.id == characterId }
?: throw SodaException("매핑을 찾을 수 없습니다: curation=$curationId, character=$characterId") ?: throw SodaException(messageKey = "admin.chat.curation.mapping_not_found")
mappingRepository.delete(target) mappingRepository.delete(target)
} }
@Transactional @Transactional
fun reorderCharacters(curationId: Long, characterIds: List<Long>) { fun reorderCharacters(curationId: Long, characterIds: List<Long>) {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
val mappings = mappingRepository.findByCuration(curation) val mappings = mappingRepository.findByCuration(curation)
val mappingByCharacterId = mappings.associateBy { it.chatCharacter.id } val mappingByCharacterId = mappings.associateBy { it.chatCharacter.id }
characterIds.forEachIndexed { index, cid -> characterIds.forEachIndexed { index, cid ->
val mapping = mappingByCharacterId[cid] val mapping = mappingByCharacterId[cid]
?: throw SodaException("큐레이션에 포함되지 않은 캐릭터입니다: $cid") ?: throw SodaException(messageKey = "admin.chat.curation.character_not_in_curation")
mapping.sortOrder = index + 1 mapping.sortOrder = index + 1
mappingRepository.save(mapping) mappingRepository.save(mapping)
} }
@@ -146,7 +146,7 @@ class CharacterCurationAdminService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun listCharacters(curationId: Long): List<ChatCharacter> { fun listCharacters(curationId: Long): List<ChatCharacter> {
val curation = curationRepository.findById(curationId) val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") } .orElseThrow { SodaException(messageKey = "admin.chat.curation.not_found") }
val mappings = mappingRepository.findByCurationWithCharacterOrderBySortOrderAsc(curation) val mappings = mappingRepository.findByCurationWithCharacterOrderBySortOrderAsc(curation)
return mappings.map { it.chatCharacter } return mappings.map { it.chatCharacter }
} }

View File

@@ -11,6 +11,8 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.image.CharacterImageService import kr.co.vividnext.sodalive.chat.character.image.CharacterImageService
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
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.utils.ImageBlurUtil import kr.co.vividnext.sodalive.utils.ImageBlurUtil
import kr.co.vividnext.sodalive.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
@@ -34,6 +36,8 @@ class AdminCharacterImageController(
private val imageService: CharacterImageService, private val imageService: CharacterImageService,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val imageCloudFront: ImageContentCloudFront, private val imageCloudFront: ImageContentCloudFront,
private val langContext: LangContext,
private val messageSource: SodaMessageSource,
@Value("\${cloud.aws.s3.content-bucket}") @Value("\${cloud.aws.s3.content-bucket}")
private val s3Bucket: String, private val s3Bucket: String,
@@ -106,14 +110,18 @@ class AdminCharacterImageController(
@DeleteMapping("/{imageId}") @DeleteMapping("/{imageId}")
fun delete(@PathVariable imageId: Long) = run { fun delete(@PathVariable imageId: Long) = run {
imageService.deleteImage(imageId) imageService.deleteImage(imageId)
ApiResponse.ok(null, "이미지가 삭제되었습니다.") val message = messageSource.getMessage("admin.chat.character.image_deleted", langContext.lang)
ApiResponse.ok(null, message)
} }
@PutMapping("/orders") @PutMapping("/orders")
fun updateOrders(@RequestBody request: UpdateCharacterImageOrdersRequest) = run { fun updateOrders(@RequestBody request: UpdateCharacterImageOrdersRequest) = run {
if (request.characterId == null) throw SodaException("characterId는 필수입니다") if (request.characterId == null) {
throw SodaException(messageKey = "admin.chat.character.character_id_required")
}
imageService.updateOrders(request.characterId, request.ids) imageService.updateOrders(request.characterId, request.ids)
ApiResponse.ok(null, "정렬 순서가 변경되었습니다.") val message = messageSource.getMessage("admin.chat.character.order_updated", langContext.lang)
ApiResponse.ok(null, message)
} }
private fun buildS3Key(characterId: Long): String { private fun buildS3Key(characterId: Long): String {
@@ -132,7 +140,7 @@ class AdminCharacterImageController(
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.character.image_save_failed")
} }
} }
@@ -141,7 +149,7 @@ class AdminCharacterImageController(
// 멀티파트를 BufferedImage로 읽기 // 멀티파트를 BufferedImage로 읽기
val bytes = image.bytes val bytes = image.bytes
val bimg = javax.imageio.ImageIO.read(java.io.ByteArrayInputStream(bytes)) val bimg = javax.imageio.ImageIO.read(java.io.ByteArrayInputStream(bytes))
?: throw SodaException("이미지 포맷을 인식할 수 없습니다.") ?: throw SodaException(messageKey = "admin.chat.character.image_format_invalid")
val blurred = ImageBlurUtil.blurFast(bimg) val blurred = ImageBlurUtil.blurFast(bimg)
// PNG로 저장(알파 유지), JPEG 업로드가 필요하면 포맷 변경 가능 // PNG로 저장(알파 유지), JPEG 업로드가 필요하면 포맷 변경 가능
@@ -164,7 +172,7 @@ class AdminCharacterImageController(
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (e: Exception) {
throw SodaException("블러 이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.character.blur_image_save_failed")
} }
} }
} }

View File

@@ -58,7 +58,7 @@ class AdminChatCharacterService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getChatCharacterDetail(characterId: Long, imageHost: String = ""): ChatCharacterDetailResponse { fun getChatCharacterDetail(characterId: Long, imageHost: String = ""): ChatCharacterDetailResponse {
val chatCharacter = chatCharacterRepository.findById(characterId) val chatCharacter = chatCharacterRepository.findById(characterId)
.orElseThrow { SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: $characterId") } .orElseThrow { SodaException(messageKey = "admin.chat.character.not_found") }
return ChatCharacterDetailResponse.from(chatCharacter, imageHost) return ChatCharacterDetailResponse.from(chatCharacter, imageHost)
} }

View File

@@ -192,8 +192,8 @@ class AdminOriginalWorkController(
filePath = "originals/$originalWorkId/${generateFileName(prefix = "original")}", filePath = "originals/$originalWorkId/${generateFileName(prefix = "original")}",
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (_: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.chat.original.image_save_failed")
} }
} }
} }

View File

@@ -38,7 +38,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun createOriginalWork(request: OriginalWorkRegisterRequest): OriginalWork { fun createOriginalWork(request: OriginalWorkRegisterRequest): OriginalWork {
originalWorkRepository.findByTitleAndIsDeletedFalse(request.title)?.let { originalWorkRepository.findByTitleAndIsDeletedFalse(request.title)?.let {
throw SodaException("동일한 제목의 원작이 이미 존재합니다: ${request.title}") throw SodaException(messageKey = "admin.chat.original.duplicate_title")
} }
val entity = OriginalWork( val entity = OriginalWork(
title = request.title, title = request.title,
@@ -107,7 +107,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun updateOriginalWork(request: OriginalWorkUpdateRequest, imagePath: String? = null): OriginalWork { fun updateOriginalWork(request: OriginalWorkUpdateRequest, imagePath: String? = null): OriginalWork {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(request.id) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(request.id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
request.title?.let { ow.title = it } request.title?.let { ow.title = it }
request.contentType?.let { ow.contentType = it } request.contentType?.let { ow.contentType = it }
@@ -177,7 +177,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun updateOriginalWorkImage(originalWorkId: Long, imagePath: String): OriginalWork { fun updateOriginalWorkImage(originalWorkId: Long, imagePath: String): OriginalWork {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
ow.imagePath = imagePath ow.imagePath = imagePath
return originalWorkRepository.save(ow) return originalWorkRepository.save(ow)
} }
@@ -186,7 +186,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun deleteOriginalWork(id: Long) { fun deleteOriginalWork(id: Long) {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(id) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다: $id") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
ow.isDeleted = true ow.isDeleted = true
originalWorkRepository.save(ow) originalWorkRepository.save(ow)
} }
@@ -195,7 +195,7 @@ class AdminOriginalWorkService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getOriginalWork(id: Long): OriginalWork { fun getOriginalWork(id: Long): OriginalWork {
return originalWorkRepository.findByIdAndIsDeletedFalse(id) return originalWorkRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
} }
/** 원작 페이징 조회 */ /** 원작 페이징 조회 */
@@ -216,7 +216,7 @@ class AdminOriginalWorkService(
fun getCharactersOfOriginalWorkPage(originalWorkId: Long, page: Int, size: Int): Page<ChatCharacter> { fun getCharactersOfOriginalWorkPage(originalWorkId: Long, page: Int, size: Int): Page<ChatCharacter> {
// 원작 존재 및 소프트 삭제 여부 확인 // 원작 존재 및 소프트 삭제 여부 확인
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
val safePage = if (page < 0) 0 else page val safePage = if (page < 0) 0 else page
val safeSize = when { val safeSize = when {
@@ -238,7 +238,7 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun assignCharacters(originalWorkId: Long, characterIds: List<Long>) { fun assignCharacters(originalWorkId: Long, characterIds: List<Long>) {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
if (characterIds.isEmpty()) return if (characterIds.isEmpty()) return
val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds) val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds)
characters.forEach { it.originalWork = ow } characters.forEach { it.originalWork = ow }
@@ -250,7 +250,7 @@ class AdminOriginalWorkService(
fun unassignCharacters(originalWorkId: Long, characterIds: List<Long>) { fun unassignCharacters(originalWorkId: Long, characterIds: List<Long>) {
// 원작 존재 확인 (소프트 삭제 제외) // 원작 존재 확인 (소프트 삭제 제외)
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
if (characterIds.isEmpty()) return if (characterIds.isEmpty()) return
val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds) val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds)
characters.forEach { it.originalWork = null } characters.forEach { it.originalWork = null }
@@ -261,13 +261,13 @@ class AdminOriginalWorkService(
@Transactional @Transactional
fun assignOneCharacter(originalWorkId: Long, characterId: Long) { fun assignOneCharacter(originalWorkId: Long, characterId: Long) {
val character = chatCharacterRepository.findById(characterId) val character = chatCharacterRepository.findById(characterId)
.orElseThrow { SodaException("해당 캐릭터를 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.character.not_found") }
if (originalWorkId == 0L) { if (originalWorkId == 0L) {
character.originalWork = null character.originalWork = null
} else { } else {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
character.originalWork = ow character.originalWork = ow
} }

View File

@@ -51,7 +51,9 @@ class AdminContentService(
searchWord: String, searchWord: String,
pageable: Pageable pageable: Pageable
): GetAdminContentListResponse { ): GetAdminContentListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") if (searchWord.length < 2) {
throw SodaException(messageKey = "admin.content.search_word_min_length")
}
val totalCount = repository.getAudioContentTotalCount(searchWord, status = status) val totalCount = repository.getAudioContentTotalCount(searchWord, status = status)
val audioContentAndThemeList = repository.getAudioContentList( val audioContentAndThemeList = repository.getAudioContentList(
status = status, status = status,
@@ -82,7 +84,7 @@ class AdminContentService(
@Transactional @Transactional
fun updateAudioContent(request: UpdateAdminContentRequest) { fun updateAudioContent(request: UpdateAdminContentRequest) {
val audioContent = repository.findByIdOrNull(id = request.id) val audioContent = repository.findByIdOrNull(id = request.id)
?: throw SodaException("없는 콘텐츠 입니다.") ?: throw SodaException(messageKey = "admin.content.not_found")
if (request.isDefaultCoverImage) { if (request.isDefaultCoverImage) {
audioContent.coverImage = "`profile/default_profile.png`" audioContent.coverImage = "`profile/default_profile.png`"

View File

@@ -33,19 +33,19 @@ class AdminContentBannerService(
fun createAudioContentMainBanner(image: MultipartFile, requestString: String) { fun createAudioContentMainBanner(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateContentBannerRequest::class.java) val request = objectMapper.readValue(requestString, CreateContentBannerRequest::class.java)
if (request.type == AudioContentBannerType.CREATOR && request.creatorId == null) { if (request.type == AudioContentBannerType.CREATOR && request.creatorId == null) {
throw SodaException("크리에이터를 선택하세요.") throw SodaException(messageKey = "admin.content.banner.creator_required")
} }
if (request.type == AudioContentBannerType.SERIES && request.seriesId == null) { if (request.type == AudioContentBannerType.SERIES && request.seriesId == null) {
throw SodaException("시리즈를 선택하세요.") throw SodaException(messageKey = "admin.content.banner.series_required")
} }
if (request.type == AudioContentBannerType.LINK && request.link == null) { if (request.type == AudioContentBannerType.LINK && request.link == null) {
throw SodaException("링크 url을 입력하세요.") throw SodaException(messageKey = "admin.content.banner.link_required")
} }
if (request.type == AudioContentBannerType.EVENT && request.eventId == null) { if (request.type == AudioContentBannerType.EVENT && request.eventId == null) {
throw SodaException("이벤트를 선택하세요.") throw SodaException(messageKey = "admin.content.banner.event_required")
} }
val event = if (request.eventId != null && request.eventId > 0) { val event = if (request.eventId != null && request.eventId > 0) {
@@ -94,7 +94,7 @@ class AdminContentBannerService(
fun updateAudioContentMainBanner(image: MultipartFile?, requestString: String) { fun updateAudioContentMainBanner(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateContentBannerRequest::class.java) val request = objectMapper.readValue(requestString, UpdateContentBannerRequest::class.java)
val audioContentBanner = repository.findByIdOrNull(request.id) val audioContentBanner = repository.findByIdOrNull(request.id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (image != null) { if (image != null) {
val fileName = generateFileName() val fileName = generateFileName()
@@ -124,22 +124,22 @@ class AdminContentBannerService(
AudioContentBannerType.EVENT -> { AudioContentBannerType.EVENT -> {
if (request.eventId != null) { if (request.eventId != null) {
val event = eventRepository.findByIdOrNull(request.eventId) val event = eventRepository.findByIdOrNull(request.eventId)
?: throw SodaException("이벤트를 선택하세요.") ?: throw SodaException(messageKey = "admin.content.banner.event_required")
audioContentBanner.event = event audioContentBanner.event = event
} else { } else {
throw SodaException("이벤트를 선택하세요.") throw SodaException(messageKey = "admin.content.banner.event_required")
} }
} }
AudioContentBannerType.CREATOR -> { AudioContentBannerType.CREATOR -> {
if (request.creatorId != null) { if (request.creatorId != null) {
val creator = memberRepository.findByIdOrNull(request.creatorId) val creator = memberRepository.findByIdOrNull(request.creatorId)
?: throw SodaException("크리에이터를 선택하세요.") ?: throw SodaException(messageKey = "admin.content.banner.creator_required")
audioContentBanner.creator = creator audioContentBanner.creator = creator
} else { } else {
throw SodaException("크리에이터를 선택하세요.") throw SodaException(messageKey = "admin.content.banner.creator_required")
} }
} }
@@ -147,18 +147,18 @@ class AdminContentBannerService(
if (request.link != null) { if (request.link != null) {
audioContentBanner.link = request.link audioContentBanner.link = request.link
} else { } else {
throw SodaException("링크 url을 입력하세요.") throw SodaException(messageKey = "admin.content.banner.link_required")
} }
} }
AudioContentBannerType.SERIES -> { AudioContentBannerType.SERIES -> {
if (request.seriesId != null) { if (request.seriesId != null) {
val series = seriesRepository.findByIdOrNull(request.seriesId) val series = seriesRepository.findByIdOrNull(request.seriesId)
?: throw SodaException("시리즈를 선택하세요.") ?: throw SodaException(messageKey = "admin.content.banner.series_required")
audioContentBanner.series = series audioContentBanner.series = series
} else { } else {
throw SodaException("시리즈를 선택하세요.") throw SodaException(messageKey = "admin.content.banner.series_required")
} }
} }
} }

View File

@@ -21,7 +21,7 @@ class AdminContentCurationService(
@Transactional @Transactional
fun createContentCuration(request: CreateContentCurationRequest) { fun createContentCuration(request: CreateContentCurationRequest) {
val tab = contentMainTabRepository.findByIdOrNull(request.tabId) val tab = contentMainTabRepository.findByIdOrNull(request.tabId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
val curation = AudioContentCuration( val curation = AudioContentCuration(
title = request.title, title = request.title,
@@ -37,7 +37,7 @@ class AdminContentCurationService(
@Transactional @Transactional
fun updateContentCuration(request: UpdateContentCurationRequest) { fun updateContentCuration(request: UpdateContentCurationRequest) {
val audioContentCuration = repository.findByIdOrNull(id = request.id) val audioContentCuration = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (request.title != null) { if (request.title != null) {
audioContentCuration.title = request.title audioContentCuration.title = request.title
@@ -85,7 +85,7 @@ class AdminContentCurationService(
fun getCurationItem(curationId: Long): List<GetCurationItemResponse> { fun getCurationItem(curationId: Long): List<GetCurationItemResponse> {
val curation = repository.findByIdOrNull(curationId) val curation = repository.findByIdOrNull(curationId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
return if (curation.isSeries) { return if (curation.isSeries) {
contentCurationItemRepository.getAudioContentCurationSeriesItemList(curationId) contentCurationItemRepository.getAudioContentCurationSeriesItemList(curationId)
@@ -106,7 +106,7 @@ class AdminContentCurationService(
fun addItemToCuration(request: AddItemToCurationRequest) { fun addItemToCuration(request: AddItemToCurationRequest) {
// 큐레이션 조회 // 큐레이션 조회
val audioContentCuration = repository.findByIdOrNull(id = request.curationId) val audioContentCuration = repository.findByIdOrNull(id = request.curationId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (audioContentCuration.isSeries) { if (audioContentCuration.isSeries) {
request.itemIdList.forEach { seriesId -> request.itemIdList.forEach { seriesId ->

View File

@@ -28,7 +28,7 @@ class AdminHashTagCurationService(
val isExists = repository.isExistsTag(tag = tag) val isExists = repository.isExistsTag(tag = tag)
if (isExists) { if (isExists) {
throw SodaException("이미 등록된 태그 입니다.") throw SodaException(messageKey = "admin.content.hash_tag.already_registered")
} }
repository.save( repository.save(
@@ -42,7 +42,7 @@ class AdminHashTagCurationService(
@Transactional @Transactional
fun updateContentHashTagCuration(request: UpdateContentHashTagCurationRequest) { fun updateContentHashTagCuration(request: UpdateContentHashTagCurationRequest) {
val hashTagCuration = repository.findByIdOrNull(id = request.id) val hashTagCuration = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (request.tag != null) { if (request.tag != null) {
var tag = request.tag.trim() var tag = request.tag.trim()
@@ -88,7 +88,7 @@ class AdminHashTagCurationService(
@Transactional @Transactional
fun addItemToHashTagCuration(request: AddItemToCurationRequest) { fun addItemToHashTagCuration(request: AddItemToCurationRequest) {
val curation = repository.findByIdOrNull(id = request.curationId) val curation = repository.findByIdOrNull(id = request.curationId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
request.itemIdList.forEach { contentId -> request.itemIdList.forEach { contentId ->
val audioContent = audioContentRepository.findByIdAndActive(contentId) val audioContent = audioContentRepository.findByIdAndActive(contentId)

View File

@@ -43,12 +43,12 @@ class AdminContentSeriesService(
@Transactional @Transactional
fun modifySeries(request: AdminModifySeriesRequest) { fun modifySeries(request: AdminModifySeriesRequest) {
val series = repository.findByIdAndActiveTrue(request.seriesId) val series = repository.findByIdAndActiveTrue(request.seriesId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (request.publishedDaysOfWeek != null) { if (request.publishedDaysOfWeek != null) {
val days = request.publishedDaysOfWeek val days = request.publishedDaysOfWeek
if (days.contains(SeriesPublishedDaysOfWeek.RANDOM) && days.size > 1) { if (days.contains(SeriesPublishedDaysOfWeek.RANDOM) && days.size > 1) {
throw SodaException("랜덤과 연재요일 동시에 선택할 수 없습니다.") throw SodaException(messageKey = "admin.content.series.random_days_conflict")
} }
series.publishedDaysOfWeek.clear() series.publishedDaysOfWeek.clear()
series.publishedDaysOfWeek.addAll(days) series.publishedDaysOfWeek.addAll(days)
@@ -56,7 +56,7 @@ class AdminContentSeriesService(
if (request.genreId != null) { if (request.genreId != null) {
val genre = genreRepository.findActiveSeriesGenreById(request.genreId) val genre = genreRepository.findActiveSeriesGenreById(request.genreId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
series.genre = genre series.genre = genre
} }

View File

@@ -11,6 +11,8 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.series.main.banner.ContentSeriesBannerService import kr.co.vividnext.sodalive.content.series.main.banner.ContentSeriesBannerService
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.utils.generateFileName import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
@@ -33,6 +35,8 @@ import org.springframework.web.multipart.MultipartFile
class AdminContentSeriesBannerController( class AdminContentSeriesBannerController(
private val bannerService: ContentSeriesBannerService, private val bannerService: ContentSeriesBannerService,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
private val langContext: LangContext,
private val messageSource: SodaMessageSource,
@Value("\${cloud.aws.s3.bucket}") @Value("\${cloud.aws.s3.bucket}")
private val s3Bucket: String, private val s3Bucket: String,
@@ -113,7 +117,8 @@ class AdminContentSeriesBannerController(
@DeleteMapping("/{bannerId}") @DeleteMapping("/{bannerId}")
fun deleteBanner(@PathVariable bannerId: Long) = run { fun deleteBanner(@PathVariable bannerId: Long) = run {
bannerService.deleteBanner(bannerId) bannerService.deleteBanner(bannerId)
ApiResponse.ok("배너가 성공적으로 삭제되었습니다.") val message = messageSource.getMessage("admin.content.series.banner.delete_success", langContext.lang)
ApiResponse.ok(message)
} }
/** /**
@@ -124,7 +129,8 @@ class AdminContentSeriesBannerController(
@RequestBody request: UpdateBannerOrdersRequest @RequestBody request: UpdateBannerOrdersRequest
) = run { ) = run {
bannerService.updateBannerOrders(request.ids) bannerService.updateBannerOrders(request.ids)
ApiResponse.ok(null, "배너 정렬 순서가 성공적으로 변경되었습니다.") val message = messageSource.getMessage("admin.content.series.banner.reorder_success", langContext.lang)
ApiResponse.ok(null, message)
} }
private fun saveImage(bannerId: Long, image: MultipartFile): String { private fun saveImage(bannerId: Long, image: MultipartFile): String {
@@ -139,7 +145,7 @@ class AdminContentSeriesBannerController(
metadata = metadata metadata = metadata
) )
} catch (e: Exception) { } catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}") throw SodaException(messageKey = "admin.content.series.banner.image_save_failed")
} }
} }
} }

View File

@@ -17,11 +17,11 @@ class AdminContentSeriesGenreService(private val repository: AdminContentSeriesG
@Transactional @Transactional
fun modifySeriesGenre(request: ModifySeriesGenreRequest) { fun modifySeriesGenre(request: ModifySeriesGenreRequest) {
if (request.genre == null && request.isAdult == null && request.isActive == null) { if (request.genre == null && request.isAdult == null && request.isActive == null) {
throw SodaException("변경사항이 없습니다.") throw SodaException(messageKey = "admin.content.series.genre.no_changes")
} }
val seriesGenre = repository.findByIdOrNull(id = request.id) val seriesGenre = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (request.genre != null) { if (request.genre != null) {
seriesGenre.genre = request.genre seriesGenre.genre = request.genre

View File

@@ -30,7 +30,7 @@ class AdminRecommendSeriesService(
fun createRecommendSeries(image: MultipartFile, requestString: String) { fun createRecommendSeries(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateRecommendSeriesRequest::class.java) val request = objectMapper.readValue(requestString, CreateRecommendSeriesRequest::class.java)
val series = seriesRepository.findByIdOrNull(request.seriesId) val series = seriesRepository.findByIdOrNull(request.seriesId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
val recommendSeries = RecommendSeries(isFree = request.isFree) val recommendSeries = RecommendSeries(isFree = request.isFree)
recommendSeries.series = series recommendSeries.series = series
@@ -49,7 +49,7 @@ class AdminRecommendSeriesService(
fun updateRecommendSeries(image: MultipartFile?, requestString: String) { fun updateRecommendSeries(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateRecommendSeriesRequest::class.java) val request = objectMapper.readValue(requestString, UpdateRecommendSeriesRequest::class.java)
val recommendSeries = repository.findByIdOrNull(request.id) val recommendSeries = repository.findByIdOrNull(request.id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (image != null) { if (image != null) {
val fileName = generateFileName() val fileName = generateFileName()

View File

@@ -53,13 +53,15 @@ class AdminContentThemeService(
} }
fun themeExistCheck(request: CreateContentThemeRequest) { fun themeExistCheck(request: CreateContentThemeRequest) {
repository.findIdByTheme(request.theme)?.let { throw SodaException("이미 등록된 테마 입니다.") } repository.findIdByTheme(request.theme)?.let {
throw SodaException(messageKey = "admin.content.theme.already_registered")
}
} }
@Transactional @Transactional
fun deleteTheme(id: Long) { fun deleteTheme(id: Long) {
val theme = repository.findByIdOrNull(id) val theme = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
theme.theme = "${theme.theme}_deleted" theme.theme = "${theme.theme}_deleted"
theme.isActive = false theme.isActive = false

View File

@@ -34,7 +34,9 @@ class AdminEventBannerService(
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 = "admin.event.banner.detail_or_link_required")
}
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter) val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter)
@@ -102,7 +104,7 @@ class AdminEventBannerService(
event.detailImage = detailImagePath event.detailImage = detailImagePath
event.popupImage = popupImagePath event.popupImage = popupImagePath
return event.id ?: throw SodaException("이벤트 등록을 하지 못했습니다.") return event.id ?: throw SodaException(messageKey = "admin.event.banner.create_failed")
} }
@Transactional @Transactional
@@ -118,10 +120,10 @@ class AdminEventBannerService(
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()
@@ -190,9 +192,9 @@ class AdminEventBannerService(
@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

@@ -38,7 +38,7 @@ class AdminChargeEventService(private val repository: AdminChargeEventRepository
@Transactional @Transactional
fun modifyChargeEvent(request: ModifyChargeEventRequest) { fun modifyChargeEvent(request: ModifyChargeEventRequest) {
val chargeEvent = repository.findByIdOrNull(request.id) val chargeEvent = repository.findByIdOrNull(request.id)
?: throw SodaException("해당하는 충전이벤트가 없습니다\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "admin.charge_event.not_found_retry")
if (request.title != null) { if (request.title != null) {
chargeEvent.title = request.title chargeEvent.title = request.title

View File

@@ -17,10 +17,10 @@ class AdminExplorerService(
) { ) {
@Transactional @Transactional
fun createExplorerSection(request: CreateExplorerSectionRequest): Long { fun createExplorerSection(request: CreateExplorerSectionRequest): Long {
if (request.title.isBlank()) throw SodaException("제목을 입력하세요.") if (request.title.isBlank()) throw SodaException(messageKey = "admin.explorer.title_required")
val findExplorerSection = repository.findByTitle(request.title) val findExplorerSection = repository.findByTitle(request.title)
if (findExplorerSection != null) throw SodaException("동일한 제목이 있습니다.") if (findExplorerSection != null) throw SodaException(messageKey = "admin.explorer.title_duplicate")
val explorerSection = ExplorerSection(title = request.title, isAdult = request.isAdult) val explorerSection = ExplorerSection(title = request.title, isAdult = request.isAdult)
explorerSection.coloredTitle = request.coloredTitle explorerSection.coloredTitle = request.coloredTitle
@@ -37,7 +37,7 @@ class AdminExplorerService(
} }
} }
if (tags.size <= 0) throw SodaException("관심사를 선택하세요.") if (tags.size <= 0) throw SodaException(messageKey = "admin.explorer.tags_required")
explorerSection.tags = tags explorerSection.tags = tags
return repository.save(explorerSection).id!! return repository.save(explorerSection).id!!
@@ -53,14 +53,14 @@ class AdminExplorerService(
request.coloredTitle == null && request.coloredTitle == null &&
request.isActive == null request.isActive == null
) { ) {
throw SodaException("변경사항이 없습니다.") throw SodaException(messageKey = "admin.explorer.no_changes")
} }
val explorerSection = repository.findByIdOrNull(request.id) val explorerSection = repository.findByIdOrNull(request.id)
?: throw SodaException("해당하는 섹션이 없습니다.") ?: throw SodaException(messageKey = "admin.explorer.section_not_found")
if (request.title != null) { if (request.title != null) {
if (request.title.isBlank()) throw SodaException("올바른 제목을 입력하세요.") if (request.title.isBlank()) throw SodaException(messageKey = "admin.explorer.valid_title_required")
explorerSection.title = request.title explorerSection.title = request.title
} }
@@ -97,7 +97,7 @@ class AdminExplorerService(
} }
} }
if (tags.size <= 0) throw SodaException("관심사를 입력하세요.") if (tags.size <= 0) throw SodaException(messageKey = "admin.explorer.tags_input_required")
if (tags != explorerSection.tags) { if (tags != explorerSection.tags) {
explorerSection.tags.clear() explorerSection.tags.clear()
explorerSection.tags.addAll(tags) explorerSection.tags.addAll(tags)

View File

@@ -15,6 +15,8 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType 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.live.recommend.RecommendLiveCreatorBanner import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBannerRepository import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBannerRepository
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
@@ -49,6 +51,8 @@ class AdminLiveService(
private val canRepository: CanRepository, private val canRepository: CanRepository,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.s3.bucket}") @Value("\${cloud.aws.s3.bucket}")
private val bucket: String, private val bucket: String,
@@ -118,10 +122,10 @@ class AdminLiveService(
endDateString: String, endDateString: String,
isAdult: Boolean isAdult: Boolean
): Long { ): Long {
if (creatorId < 1) throw SodaException("올바른 크리에이터를 선택해 주세요.") if (creatorId < 1) throw SodaException(messageKey = "admin.live.creator_required")
val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId) val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId)
?: throw SodaException("올바른 크리에이터를 선택해 주세요.") ?: throw SodaException(messageKey = "admin.live.creator_required")
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter) val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter)
@@ -134,15 +138,15 @@ class AdminLiveService(
.withZoneSameInstant(ZoneId.of("UTC")) .withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime() .toLocalDateTime()
if (startDate < nowDate) throw SodaException("노출 시작일은 현재시간 이후로 설정하셔야 합니다.") if (startDate < nowDate) throw SodaException(messageKey = "admin.live.start_after_now")
val endDate = LocalDateTime.parse(endDateString, dateTimeFormatter) val endDate = LocalDateTime.parse(endDateString, dateTimeFormatter)
.atZone(ZoneId.of("Asia/Seoul")) .atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC")) .withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime() .toLocalDateTime()
if (endDate < nowDate) throw SodaException("노출 종료일은 현재시간 이후로 설정하셔야 합니다.") if (endDate < nowDate) throw SodaException(messageKey = "admin.live.end_after_now")
if (endDate <= startDate) throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.") if (endDate <= startDate) throw SodaException(messageKey = "admin.live.start_before_end")
val recommendCreatorBanner = RecommendLiveCreatorBanner( val recommendCreatorBanner = RecommendLiveCreatorBanner(
startDate = startDate, startDate = startDate,
@@ -176,13 +180,13 @@ class AdminLiveService(
isAdult: Boolean? isAdult: Boolean?
) { ) {
val recommendCreatorBanner = recommendCreatorBannerRepository.findByIdOrNull(recommendCreatorBannerId) val recommendCreatorBanner = recommendCreatorBannerRepository.findByIdOrNull(recommendCreatorBannerId)
?: throw SodaException("해당하는 추천라이브가 없습니다. 다시 확인해 주세요.") ?: throw SodaException(messageKey = "admin.live.recommend_not_found_retry")
if (creatorId != null) { if (creatorId != null) {
if (creatorId < 1) throw SodaException("올바른 크리에이터를 선택해 주세요.") if (creatorId < 1) throw SodaException(messageKey = "admin.live.creator_required")
val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId) val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId)
?: throw SodaException("올바른 크리에이터를 선택해 주세요.") ?: throw SodaException(messageKey = "admin.live.creator_required")
recommendCreatorBanner.creator = creator recommendCreatorBanner.creator = creator
} }
@@ -218,13 +222,13 @@ class AdminLiveService(
if (endDate != null) { if (endDate != null) {
if (endDate <= startDate) { if (endDate <= startDate) {
throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.") throw SodaException(messageKey = "admin.live.start_before_end")
} }
recommendCreatorBanner.endDate = endDate recommendCreatorBanner.endDate = endDate
} else { } else {
if (recommendCreatorBanner.endDate <= startDate) { if (recommendCreatorBanner.endDate <= startDate) {
throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.") throw SodaException(messageKey = "admin.live.start_before_end")
} }
} }
@@ -237,7 +241,7 @@ class AdminLiveService(
.toLocalDateTime() .toLocalDateTime()
if (endDate <= recommendCreatorBanner.startDate) { if (endDate <= recommendCreatorBanner.startDate) {
throw SodaException("노출 종료일은 노출 시작일 이후로 설정하셔야 합니다.") throw SodaException(messageKey = "admin.live.end_after_start")
} }
recommendCreatorBanner.endDate = endDate recommendCreatorBanner.endDate = endDate
@@ -266,7 +270,10 @@ class AdminLiveService(
for (room in findRoomList) { for (room in findRoomList) {
room.isActive = false room.isActive = false
val roomCancel = LiveRoomCancel("관리자에 의한 취소 - 노쇼") val cancelReason = messageSource
.getMessage("admin.live.cancel_reason.no_show", langContext.lang)
.orEmpty()
val roomCancel = LiveRoomCancel(cancelReason)
roomCancel.room = room roomCancel.room = room
roomCancelRepository.save(roomCancel) roomCancelRepository.save(roomCancel)
@@ -286,7 +293,10 @@ class AdminLiveService(
it.status = UseCanCalculateStatus.REFUND it.status = UseCanCalculateStatus.REFUND
val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE)
charge.title = "${it.can}" val canTitleTemplate = messageSource
.getMessage("live.room.can_title", langContext.lang)
.orEmpty()
charge.title = String.format(canTitleTemplate, it.can)
charge.useCan = useCan charge.useCan = useCan
when (it.paymentGateway) { when (it.paymentGateway) {
@@ -300,7 +310,9 @@ class AdminLiveService(
status = PaymentStatus.COMPLETE, status = PaymentStatus.COMPLETE,
paymentGateway = it.paymentGateway paymentGateway = it.paymentGateway
) )
payment.method = "환불" payment.method = messageSource
.getMessage("live.room.refund_method", langContext.lang)
.orEmpty()
charge.payment = payment charge.payment = payment
chargeRepository.save(charge) chargeRepository.save(charge)
@@ -313,11 +325,15 @@ class AdminLiveService(
reservationRepository.cancelReservation(roomId = room.id!!) reservationRepository.cancelReservation(roomId = room.id!!)
// 라이브 취소 푸시 발송 // 라이브 취소 푸시 발송
val cancelMessageTemplate = messageSource
.getMessage("live.room.fcm.message.canceled", langContext.lang)
.orEmpty()
val cancelMessage = String.format(cancelMessageTemplate, room.title)
applicationEventPublisher.publishEvent( applicationEventPublisher.publishEvent(
FcmEvent( FcmEvent(
type = FcmEventType.CANCEL_LIVE, type = FcmEventType.CANCEL_LIVE,
title = room.member!!.nickname, title = room.member!!.nickname,
message = "라이브 취소 : ${room.title}", message = cancelMessage,
recipientsMap = pushTokenListMap recipientsMap = pushTokenListMap
) )
) )

View File

@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.admin.live.signature
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
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.signature.SignatureCanSortType import kr.co.vividnext.sodalive.live.signature.SignatureCanSortType
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
@@ -16,7 +18,11 @@ import org.springframework.web.multipart.MultipartFile
@RestController @RestController
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/live/signature-can") @RequestMapping("/admin/live/signature-can")
class AdminSignatureCanController(private val service: AdminSignatureCanService) { class AdminSignatureCanController(
private val service: AdminSignatureCanService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
@GetMapping @GetMapping
fun getSignatureCanList( fun getSignatureCanList(
pageable: Pageable, pageable: Pageable,
@@ -32,7 +38,7 @@ class AdminSignatureCanController(private val service: AdminSignatureCanService)
@RequestParam("isAdult", required = false) isAdult: Boolean = false @RequestParam("isAdult", required = false) isAdult: Boolean = false
) = ApiResponse.ok( ) = ApiResponse.ok(
service.createSignatureCan(can = can, time = time, creatorId = creatorId, image = image, isAdult = isAdult), service.createSignatureCan(can = can, time = time, creatorId = creatorId, image = image, isAdult = isAdult),
"등록되었습니다." messageSource.getMessage("admin.signature_can.created", langContext.lang)
) )
@PutMapping @PutMapping
@@ -45,7 +51,7 @@ class AdminSignatureCanController(private val service: AdminSignatureCanService)
@RequestParam("isAdult", required = false) isAdult: Boolean? @RequestParam("isAdult", required = false) isAdult: Boolean?
) = run { ) = run {
if (can == null && time == null && image == null && isActive == null && isAdult == null) { if (can == null && time == null && image == null && isActive == null && isAdult == null) {
throw SodaException("변경사항이 없습니다.") throw SodaException(messageKey = "admin.signature_can.no_changes")
} }
ApiResponse.ok( ApiResponse.ok(
@@ -57,7 +63,7 @@ class AdminSignatureCanController(private val service: AdminSignatureCanService)
isActive = isActive, isActive = isActive,
isAdult = isAdult isAdult = isAdult
), ),
"수정되었습니다." messageSource.getMessage("admin.signature_can.updated", langContext.lang)
) )
} }
} }

View File

@@ -43,12 +43,12 @@ class AdminSignatureCanService(
@Transactional @Transactional
fun createSignatureCan(can: Int, time: Int, creatorId: Long, image: MultipartFile, isAdult: Boolean) { fun createSignatureCan(can: Int, time: Int, creatorId: Long, image: MultipartFile, isAdult: Boolean) {
if (creatorId < 1) throw SodaException("올바른 크리에이터를 선택해 주세요.") if (creatorId < 1) throw SodaException(messageKey = "admin.signature_can.creator_required")
if (can <= 0) throw SodaException("1캔 이상 설정할 수 있습니다.") if (can <= 0) throw SodaException(messageKey = "admin.signature_can.min_can")
if (time < 3 || time > 20) throw SodaException("시간은 3초 이상 20초 이하로 설정할 수 있습니다.") if (time < 3 || time > 20) throw SodaException(messageKey = "admin.signature_can.time_range")
val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId) val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId)
?: throw SodaException("올바른 크리에이터를 선택해 주세요.") ?: throw SodaException(messageKey = "admin.signature_can.creator_required")
val signatureCan = SignatureCan(can = can, isAdult = isAdult) val signatureCan = SignatureCan(can = can, isAdult = isAdult)
signatureCan.creator = creator signatureCan.creator = creator
@@ -76,15 +76,15 @@ class AdminSignatureCanService(
isAdult: Boolean? isAdult: Boolean?
) { ) {
val signatureCan = repository.findByIdOrNull(id = id) val signatureCan = repository.findByIdOrNull(id = id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (can != null) { if (can != null) {
if (can <= 0) throw SodaException("1캔 이상 설정할 수 있습니다.") if (can <= 0) throw SodaException(messageKey = "admin.signature_can.min_can")
signatureCan.can = can signatureCan.can = can
} }
if (time != null) { if (time != null) {
if (time < 3 || time > 20) throw SodaException("시간은 3초 이상 20초 이하로 설정할 수 있습니다.") if (time < 3 || time > 20) throw SodaException(messageKey = "admin.signature_can.time_range")
signatureCan.time = time signatureCan.time = time
} }

View File

@@ -20,7 +20,7 @@ class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepositor
@Transactional @Transactional
fun updateMediaPartner(request: UpdateAdMediaPartnerRequest) { fun updateMediaPartner(request: UpdateAdMediaPartnerRequest) {
val entity = repository.findByIdOrNull(request.id) val entity = repository.findByIdOrNull(request.id)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "admin.media_partner.invalid_access")
if (request.mediaGroup != null) { if (request.mediaGroup != null) {
entity.mediaGroup = request.mediaGroup entity.mediaGroup = request.mediaGroup

View File

@@ -1,6 +1,8 @@
package kr.co.vividnext.sodalive.admin.member package kr.co.vividnext.sodalive.admin.member
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.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberProvider import kr.co.vividnext.sodalive.member.MemberProvider
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
@@ -17,6 +19,8 @@ import java.time.format.DateTimeFormatter
class AdminMemberService( class AdminMemberService(
private val repository: AdminMemberRepository, private val repository: AdminMemberRepository,
private val passwordEncoder: PasswordEncoder, private val passwordEncoder: PasswordEncoder,
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
@@ -24,7 +28,7 @@ class AdminMemberService(
@Transactional @Transactional
fun updateMember(request: UpdateMemberRequest) { fun updateMember(request: UpdateMemberRequest) {
val member = repository.findByIdOrNull(request.id) val member = repository.findByIdOrNull(request.id)
?: throw SodaException("해당 유저가 없습니다.") ?: throw SodaException(messageKey = "admin.member.not_found")
if (member.role != request.userType) { if (member.role != request.userType) {
member.role = request.userType member.role = request.userType
@@ -44,7 +48,7 @@ class AdminMemberService(
} }
fun searchMember(searchWord: String, pageable: Pageable): GetAdminMemberListResponse { fun searchMember(searchWord: String, pageable: Pageable): GetAdminMemberListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") if (searchWord.length < 2) throw SodaException(messageKey = "admin.member.search_word_min_length")
val totalCount = repository.searchMemberTotalCount(searchWord = searchWord) val totalCount = repository.searchMemberTotalCount(searchWord = searchWord)
val memberList = processMemberListToGetAdminMemberListResponseItemList( val memberList = processMemberListToGetAdminMemberListResponseItemList(
memberList = repository.searchMember( memberList = repository.searchMember(
@@ -71,7 +75,7 @@ class AdminMemberService(
} }
fun searchCreator(searchWord: String, pageable: Pageable): GetAdminMemberListResponse { fun searchCreator(searchWord: String, pageable: Pageable): GetAdminMemberListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") if (searchWord.length < 2) throw SodaException(messageKey = "admin.member.search_word_min_length")
val totalCount = repository.searchMemberTotalCount(searchWord = searchWord, role = MemberRole.CREATOR) val totalCount = repository.searchMemberTotalCount(searchWord = searchWord, role = MemberRole.CREATOR)
val creatorList = processMemberListToGetAdminMemberListResponseItemList( val creatorList = processMemberListToGetAdminMemberListResponseItemList(
memberList = repository.searchMember( memberList = repository.searchMember(
@@ -92,18 +96,18 @@ class AdminMemberService(
.asSequence() .asSequence()
.map { .map {
val userType = when (it.role) { val userType = when (it.role) {
MemberRole.ADMIN -> "관리자" MemberRole.ADMIN -> messageSource.getMessage("admin.member.role.admin", langContext.lang).orEmpty()
MemberRole.USER -> "일반회원" MemberRole.USER -> messageSource.getMessage("admin.member.role.user", langContext.lang).orEmpty()
MemberRole.CREATOR -> "크리에이터" MemberRole.CREATOR -> messageSource.getMessage("admin.member.role.creator", langContext.lang).orEmpty()
MemberRole.AGENT -> "에이전트" MemberRole.AGENT -> messageSource.getMessage("admin.member.role.agent", langContext.lang).orEmpty()
MemberRole.BOT -> "" MemberRole.BOT -> messageSource.getMessage("admin.member.role.bot", langContext.lang).orEmpty()
} }
val loginType = when (it.provider) { val loginType = when (it.provider) {
MemberProvider.EMAIL -> "이메일" MemberProvider.EMAIL -> messageSource.getMessage("member.provider.email", langContext.lang).orEmpty()
MemberProvider.KAKAO -> "카카오" MemberProvider.KAKAO -> messageSource.getMessage("member.provider.kakao", langContext.lang).orEmpty()
MemberProvider.GOOGLE -> "구글" MemberProvider.GOOGLE -> messageSource.getMessage("member.provider.google", langContext.lang).orEmpty()
MemberProvider.APPLE -> "애플" MemberProvider.APPLE -> messageSource.getMessage("member.provider.apple", langContext.lang).orEmpty()
} }
val signUpDate = it.createdAt!! val signUpDate = it.createdAt!!
@@ -146,7 +150,7 @@ class AdminMemberService(
} }
fun searchMemberByNickname(searchWord: String, size: Int = 20): List<AdminSimpleMemberResponse> { fun searchMemberByNickname(searchWord: String, size: Int = 20): List<AdminSimpleMemberResponse> {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") if (searchWord.length < 2) throw SodaException(messageKey = "admin.member.search_word_min_length")
val limit = if (size <= 0) 20 else size val limit = if (size <= 0) 20 else size
return repository.searchMemberByNickname(searchWord = searchWord, limit = limit.toLong()) return repository.searchMemberByNickname(searchWord = searchWord, limit = limit.toLong())
} }
@@ -154,7 +158,7 @@ class AdminMemberService(
@Transactional @Transactional
fun resetPassword(request: ResetPasswordRequest) { fun resetPassword(request: ResetPasswordRequest) {
val member = repository.findByIdAndActive(memberId = request.memberId) val member = repository.findByIdAndActive(memberId = request.memberId)
?: throw SodaException("잘못된 회원정보입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "admin.member.reset_password_invalid")
member.password = passwordEncoder.encode(member.email.split("@")[0]) member.password = passwordEncoder.encode(member.email.split("@")[0])
} }

View File

@@ -35,7 +35,9 @@ class AdminMemberTagService(
} }
private fun tagExistCheck(request: CreateMemberTagRequest) { private fun tagExistCheck(request: CreateMemberTagRequest) {
repository.findByTag(request.tag)?.let { throw SodaException("이미 등록된 태그 입니다.") } repository.findByTag(request.tag)?.let {
throw SodaException(messageKey = "admin.member.tag.already_registered")
}
} }
private fun createTag(tag: String, imagePath: String, isAdult: Boolean) { private fun createTag(tag: String, imagePath: String, isAdult: Boolean) {
@@ -51,7 +53,7 @@ class AdminMemberTagService(
@Transactional @Transactional
fun deleteTag(id: Long) { fun deleteTag(id: Long) {
val creatorTag = repository.findByIdOrNull(id) val creatorTag = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
creatorTag.tag = "${creatorTag.tag}_deleted" creatorTag.tag = "${creatorTag.tag}_deleted"
creatorTag.isActive = false creatorTag.isActive = false
@@ -60,7 +62,7 @@ class AdminMemberTagService(
@Transactional @Transactional
fun modifyTag(id: Long, image: MultipartFile?, requestString: String) { fun modifyTag(id: Long, image: MultipartFile?, requestString: String) {
val creatorTag = repository.findByIdOrNull(id) val creatorTag = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
val request = objectMapper.readValue(requestString, CreateMemberTagRequest::class.java) val request = objectMapper.readValue(requestString, CreateMemberTagRequest::class.java)
creatorTag.tag = request.tag creatorTag.tag = request.tag

View File

@@ -26,7 +26,7 @@ class PointPolicyService(private val repository: PointPolicyRepository) {
@Transactional @Transactional
fun update(id: Long, request: ModifyPointRewardPolicyRequest) { fun update(id: Long, request: ModifyPointRewardPolicyRequest) {
val pointPolicy = repository.findByIdOrNull(id) val pointPolicy = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 접근입니다.") ?: throw SodaException(messageKey = "admin.point.policy.invalid_access")
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")

View File

@@ -32,7 +32,7 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
) )
if (dateRange == null) { if (dateRange == null) {
throw SodaException("잘못된 접근입니다.") throw SodaException(messageKey = "admin.member.statistics.invalid_access")
} }
var startDateTime = startDate.atStartOfDay() var startDateTime = startDate.atStartOfDay()

View File

@@ -17,7 +17,7 @@ class AlarmController(private val service: AlarmService) {
fun getSlotQuantityAndPrice( fun getSlotQuantityAndPrice(
@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( ApiResponse.ok(
service.getSlotQuantityAndPrice(memberId = member.id!!) service.getSlotQuantityAndPrice(memberId = member.id!!)
@@ -29,7 +29,7 @@ class AlarmController(private val service: AlarmService) {
@PathVariable("container") container: String, @PathVariable("container") container: 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( ApiResponse.ok(
service.buyExtraSlot( service.buyExtraSlot(

View File

@@ -53,7 +53,7 @@ class AlarmService(
} }
else -> { else -> {
throw SodaException("이미 구매하셨습니다") throw SodaException(messageKey = "alarm.error.already_purchased")
} }
} }

View File

@@ -32,7 +32,7 @@ class AuditionController(private val service: AuditionService) {
@PathVariable id: Long, @PathVariable id: Long,
@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.getAuditionDetail(auditionId = id)) ApiResponse.ok(service.getAuditionDetail(auditionId = id))
} }

View File

@@ -23,7 +23,7 @@ class AuditionApplicantController(private val service: AuditionApplicantService)
@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( ApiResponse.ok(
service.getAuditionApplicantList( service.getAuditionApplicantList(
@@ -42,7 +42,7 @@ class AuditionApplicantController(private val service: AuditionApplicantService)
@RequestPart("request") requestString: String, @RequestPart("request") requestString: 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( ApiResponse.ok(
service.applyAuditionRole( service.applyAuditionRole(

View File

@@ -47,11 +47,11 @@ class AuditionApplicantService(
@Transactional @Transactional
fun applyAuditionRole(contentFile: MultipartFile?, requestString: String, member: Member) { fun applyAuditionRole(contentFile: MultipartFile?, requestString: String, member: Member) {
if (contentFile == null) throw SodaException("녹음 파일을 확인해 주세요.") if (contentFile == null) throw SodaException(messageKey = "audition.applicant.content_file_required")
val request = objectMapper.readValue(requestString, ApplyAuditionRoleRequest::class.java) val request = objectMapper.readValue(requestString, ApplyAuditionRoleRequest::class.java)
val auditionRole = roleRepository.findByIdOrNull(id = request.roleId) val auditionRole = roleRepository.findByIdOrNull(id = request.roleId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "audition.error.invalid_request_retry")
val existingApplicant = repository.findActiveApplicantByMemberIdAndRoleId( val existingApplicant = repository.findActiveApplicantByMemberIdAndRoleId(
memberId = member.id!!, memberId = member.id!!,

View File

@@ -17,7 +17,7 @@ class AuditionRoleController(private val service: AuditionRoleService) {
@PathVariable id: Long, @PathVariable id: Long,
@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( ApiResponse.ok(
service.getAuditionRoleDetail( service.getAuditionRoleDetail(

View File

@@ -11,7 +11,7 @@ class AuditionRoleService(
) { ) {
fun getAuditionRoleDetail(auditionRoleId: Long, memberId: Long): GetAuditionRoleDetailResponse { fun getAuditionRoleDetail(auditionRoleId: Long, memberId: Long): GetAuditionRoleDetailResponse {
val roleDetailData = repository.getAuditionRoleDetail(auditionRoleId = auditionRoleId) val roleDetailData = repository.getAuditionRoleDetail(auditionRoleId = auditionRoleId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "audition.error.invalid_request_retry")
val isAlreadyApplicant = applicantRepository.isAlreadyApplicant( val isAlreadyApplicant = applicantRepository.isAlreadyApplicant(
auditionRoleId = auditionRoleId, auditionRoleId = auditionRoleId,

View File

@@ -19,7 +19,7 @@ class AuditionVoteController(
@RequestBody request: VoteAuditionApplicantRequest, @RequestBody request: VoteAuditionApplicantRequest,
@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( ApiResponse.ok(
service.voteAuditionApplicant( service.voteAuditionApplicant(

View File

@@ -20,7 +20,7 @@ class AuditionVoteService(
) { ) {
fun voteAuditionApplicant(applicantId: Long, timezone: String, container: String, member: Member) { fun voteAuditionApplicant(applicantId: Long, timezone: String, container: String, member: Member) {
val applicant = applicantRepository.findByIdOrNull(applicantId) val applicant = applicantRepository.findByIdOrNull(applicantId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "audition.error.invalid_request_retry")
val defaultZoneId = ZoneId.of("Asia/Seoul") val defaultZoneId = ZoneId.of("Asia/Seoul")
val clientZoneId = try { val clientZoneId = try {
@@ -43,7 +43,7 @@ class AuditionVoteService(
) )
if (voteCount > 100) { if (voteCount > 100) {
throw SodaException("오늘 응원은 여기까지!\n하루 최대 100회까지 응원이 가능합니다.\n내일 다시 이용해주세요.") throw SodaException(messageKey = "audition.vote.max_daily_reached")
} }
if (voteCount > 0) { if (voteCount > 0) {

View File

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

View File

@@ -33,7 +33,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
ApiResponse.ok(service.payverseCharge(member, request)) ApiResponse.ok(service.payverseCharge(member, request))
@@ -45,7 +45,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
val response = service.payverseVerify(memberId = member.id!!, verifyRequest) val response = service.payverseVerify(memberId = member.id!!, verifyRequest)
@@ -83,7 +83,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
ApiResponse.ok(service.charge(member, chargeRequest)) ApiResponse.ok(service.charge(member, chargeRequest))
@@ -95,7 +95,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
val response = service.verify(memberId = member.id!!, verifyRequest) val response = service.verify(memberId = member.id!!, verifyRequest)
@@ -109,7 +109,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
val response = service.verifyHecto(memberId = member.id!!, verifyRequest) val response = service.verifyHecto(memberId = member.id!!, verifyRequest)
@@ -123,7 +123,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
ApiResponse.ok(service.appleCharge(member, chargeRequest)) ApiResponse.ok(service.appleCharge(member, chargeRequest))
@@ -135,7 +135,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
val response = service.appleVerify(memberId = member.id!!, verifyRequest) val response = service.appleVerify(memberId = member.id!!, verifyRequest)
@@ -149,7 +149,7 @@ class ChargeController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
if (request.paymentGateway == PaymentGateway.GOOGLE_IAP) { if (request.paymentGateway == PaymentGateway.GOOGLE_IAP) {
@@ -174,7 +174,7 @@ class ChargeController(
trackingCharge(member, response) trackingCharge(member, response)
ApiResponse.ok(Unit) ApiResponse.ok(Unit)
} else { } 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.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.google.GooglePlayService 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.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.point.MemberPoint import kr.co.vividnext.sodalive.point.MemberPoint
@@ -53,6 +55,8 @@ class ChargeService(
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val googlePlayService: GooglePlayService, private val googlePlayService: GooglePlayService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${bootpay.application-id}") @Value("\${bootpay.application-id}")
private val bootpayApplicationId: String, private val bootpayApplicationId: String,
@@ -174,10 +178,10 @@ class ChargeService(
@Transactional @Transactional
fun chargeByCoupon(couponNumber: String, member: Member): String { fun chargeByCoupon(couponNumber: String, member: Member): String {
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber) val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.") ?: throw SodaException(messageKey = "can.coupon.invalid_number_contact")
if (canCouponNumber.member != null) { if (canCouponNumber.member != null) {
throw SodaException("이미 사용한 쿠폰번호 입니다.") throw SodaException(messageKey = "can.coupon.already_used")
} }
canCouponNumber.member = member canCouponNumber.member = member
@@ -186,7 +190,7 @@ class ChargeService(
when (coupon.couponType) { when (coupon.couponType) {
CouponType.CAN -> { CouponType.CAN -> {
val couponCharge = Charge(0, coupon.can, status = ChargeStatus.COUPON) val couponCharge = Charge(0, coupon.can, status = ChargeStatus.COUPON)
couponCharge.title = "${coupon.can}" couponCharge.title = formatMessage("can.charge.title", coupon.can)
couponCharge.member = member couponCharge.member = member
val payment = Payment( val payment = Payment(
@@ -198,7 +202,7 @@ class ChargeService(
chargeRepository.save(couponCharge) chargeRepository.save(couponCharge)
member.charge(0, coupon.can, "pg") member.charge(0, coupon.can, "pg")
return "쿠폰 사용이 완료되었습니다.\n${coupon.can}캔이 지급되었습니다." return formatMessage("can.coupon.use_complete", coupon.can)
} }
CouponType.POINT -> { 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 @Transactional
fun payverseCharge(member: Member, request: PayverseChargeRequest): PayverseChargeResponse { fun payverseCharge(member: Member, request: PayverseChargeRequest): PayverseChargeResponse {
val can = canRepository.findByIdOrNull(request.canId) val can = canRepository.findByIdOrNull(request.canId)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") ?: throw SodaException(messageKey = "can.charge.invalid_request_restart")
val requestCurrency = can.currency val requestCurrency = can.currency
val isKrw = requestCurrency == "KRW" val isKrw = requestCurrency == "KRW"
@@ -304,9 +308,9 @@ class ChargeService(
@Transactional @Transactional
fun payverseVerify(memberId: Long, verifyRequest: PayverseVerifyRequest): ChargeCompleteResponse { fun payverseVerify(memberId: Long, verifyRequest: PayverseVerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
val isKrw = charge.can?.currency == "KRW" val isKrw = charge.can?.currency == "KRW"
val mid = if (isKrw) { val mid = if (isKrw) {
@@ -322,7 +326,7 @@ class ChargeService(
// 결제수단 확인 // 결제수단 확인
if (charge.payment?.paymentGateway != PaymentGateway.PAYVERSE) { 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() val response = okHttpClient.newCall(request).execute()
if (!response.isSuccessful) { 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 verifyResponse = objectMapper.readValue(body, PayverseVerifyResponse::class.java)
val customerId = "${serverEnv}_user_${member.id!!}" val customerId = "${serverEnv}_user_${member.id!!}"
@@ -380,10 +385,10 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId) isFirstCharged = chargeRepository.isFirstCharged(memberId)
) )
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} catch (_: Exception) { } catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
@@ -397,7 +402,7 @@ class ChargeService(
} }
else -> { else -> {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
} }
@@ -405,7 +410,7 @@ class ChargeService(
@Transactional @Transactional
fun charge(member: Member, request: ChargeRequest): ChargeResponse { fun charge(member: Member, request: ChargeRequest): ChargeResponse {
val can = canRepository.findByIdOrNull(request.canId) val can = canRepository.findByIdOrNull(request.canId)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") ?: throw SodaException(messageKey = "can.charge.invalid_request_restart")
val charge = Charge(can.can, can.rewardCan) val charge = Charge(can.can, can.rewardCan)
charge.title = can.title charge.title = can.title
@@ -424,9 +429,9 @@ class ChargeService(
@Transactional @Transactional
fun verify(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse { fun verify(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) { if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey) val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)
@@ -457,22 +462,22 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId) isFirstCharged = chargeRepository.isFirstCharged(memberId)
) )
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} catch (_: Exception) { } catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
@Transactional @Transactional
fun verifyHecto(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse { fun verifyHecto(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) { if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
val bootpay = Bootpay(bootpayHectoApplicationId, bootpayHectoPrivateKey) val bootpay = Bootpay(bootpayHectoApplicationId, bootpayHectoPrivateKey)
@@ -507,13 +512,13 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId) isFirstCharged = chargeRepository.isFirstCharged(memberId)
) )
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} catch (_: Exception) { } catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
@@ -542,15 +547,17 @@ class ChargeService(
@Transactional @Transactional
fun appleVerify(memberId: Long, verifyRequest: AppleVerifyRequest): ChargeCompleteResponse { fun appleVerify(memberId: Long, verifyRequest: AppleVerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId) val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId)
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(memberId) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) { if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) {
// 검증로직 // 검증로직
if (requestRealServerVerify(verifyRequest)) { if (requestRealServerVerify(verifyRequest)) {
charge.payment?.receiptId = verifyRequest.receiptString 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 charge.payment?.status = PaymentStatus.COMPLETE
member.charge(charge.chargeCan, charge.rewardCan, "ios") member.charge(charge.chargeCan, charge.rewardCan, "ios")
@@ -567,10 +574,10 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId) isFirstCharged = chargeRepository.isFirstCharged(memberId)
) )
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
@@ -594,7 +601,9 @@ class ChargeService(
payment.locale = currencyCode payment.locale = currencyCode
payment.price = price payment.price = price
payment.receiptId = purchaseToken payment.receiptId = purchaseToken
payment.method = "구글(인 앱 결제)" payment.method = messageSource
.getMessage("can.charge.payment_method.google_iap", langContext.lang)
.orEmpty()
charge.payment = payment charge.payment = payment
chargeRepository.save(charge) chargeRepository.save(charge)
@@ -610,9 +619,9 @@ class ChargeService(
purchaseToken: String purchaseToken: String
): ChargeCompleteResponse { ): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(id = chargeId) val charge = chargeRepository.findByIdOrNull(id = chargeId)
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.status == PaymentStatus.REQUEST) { if (charge.payment!!.status == PaymentStatus.REQUEST) {
val orderId = verifyPurchase(purchaseToken, productId) val orderId = verifyPurchase(purchaseToken, productId)
@@ -634,10 +643,10 @@ class ChargeService(
isFirstCharged = chargeRepository.isFirstCharged(memberId) isFirstCharged = chargeRepository.isFirstCharged(memberId)
) )
} else { } else {
throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요") throw SodaException(messageKey = "can.charge.purchase_failed_contact")
} }
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
@@ -670,14 +679,14 @@ class ChargeService(
} }
else -> { else -> {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
} else { } else {
throw SodaException("결제를 완료하지 못했습니다.") throw SodaException(messageKey = "can.charge.payment_incomplete")
} }
} else { } else {
throw SodaException("결제를 완료하지 못했습니다.") throw SodaException(messageKey = "can.charge.payment_incomplete")
} }
} }
@@ -701,23 +710,31 @@ class ChargeService(
} }
else -> { else -> {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} }
} else { } else {
throw SodaException("결제를 완료하지 못했습니다.") throw SodaException(messageKey = "can.charge.payment_incomplete")
} }
} else { } 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 반환 // Payverse 결제수단 매핑: 특정 schemeCode는 "카드"로 표기, 아니면 null 반환
private fun mapPayverseSchemeToMethodByCode(schemeCode: String?): String? { private fun mapPayverseSchemeToMethodByCode(schemeCode: String?): String? {
val cardCodes = setOf( val cardCodes = setOf(
"041", "044", "361", "364", "365", "366", "367", "368", "369", "370", "371", "372", "373", "374", "381", "041", "044", "361", "364", "365", "366", "367", "368", "369", "370", "371", "372", "373", "374", "381",
"218", "071", "002", "089", "045", "050", "048", "090", "092" "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.common.SodaException
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType 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.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.auth.AuthRepository import kr.co.vividnext.sodalive.member.auth.AuthRepository
@@ -26,15 +28,17 @@ class ChargeEventService(
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val chargeRepository: ChargeRepository, private val chargeRepository: ChargeRepository,
private val chargeEventRepository: ChargeEventRepository, private val chargeEventRepository: ChargeEventRepository,
private val applicationEventPublisher: ApplicationEventPublisher private val applicationEventPublisher: ApplicationEventPublisher,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) { ) {
@Transactional @Transactional
fun applyChargeEvent(chargeId: Long, memberId: Long) { fun applyChargeEvent(chargeId: Long, memberId: Long) {
val charge = chargeRepository.findByIdOrNull(chargeId) val charge = chargeRepository.findByIdOrNull(chargeId)
?: throw SodaException("이벤트가 적용되지 않았습니다.\n고객센터에 문의해 주세요.") ?: throw SodaException(messageKey = "can.charge.event.not_applied_contact")
val member = memberRepository.findByIdOrNull(memberId) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("이벤트가 적용되지 않았습니다.\n고객센터에 문의해 주세요.") ?: throw SodaException(messageKey = "can.charge.event.not_applied_contact")
if (member.auth != null) { if (member.auth != null) {
val authDate = authRepository.getOldestCreatedAtByDi(member.auth!!.di) val authDate = authRepository.getOldestCreatedAtByDi(member.auth!!.di)
@@ -79,7 +83,10 @@ class ChargeEventService(
FcmEvent( FcmEvent(
type = FcmEventType.INDIVIDUAL, type = FcmEventType.INDIVIDUAL,
title = chargeEvent.title, title = chargeEvent.title,
message = "$additionalCan 캔이 추가 지급되었습니다.", message = formatMessage(
"can.charge.event.additional_can_paid",
additionalCan
),
recipients = listOf(member.id!!), recipients = listOf(member.id!!),
isAuth = null isAuth = null
) )
@@ -94,14 +101,21 @@ class ChargeEventService(
additionalCan = additionalCan, additionalCan = additionalCan,
member = member, member = member,
paymentGateway = charge.payment?.paymentGateway!!, paymentGateway = charge.payment?.paymentGateway!!,
method = "첫 충전 이벤트" method = messageSource
.getMessage("can.charge.event.first_title", langContext.lang)
.orEmpty()
) )
applicationEventPublisher.publishEvent( applicationEventPublisher.publishEvent(
FcmEvent( FcmEvent(
type = FcmEventType.INDIVIDUAL, type = FcmEventType.INDIVIDUAL,
title = "첫 충전 이벤트", title = messageSource
message = "$additionalCan 캔이 추가 지급되었습니다.", .getMessage("can.charge.event.first_title", langContext.lang)
.orEmpty(),
message = formatMessage(
"can.charge.event.additional_can_paid",
additionalCan
),
recipients = listOf(member.id!!), recipients = listOf(member.id!!),
isAuth = null isAuth = null
) )
@@ -110,7 +124,7 @@ class ChargeEventService(
private fun applyEvent(additionalCan: Int, member: Member, paymentGateway: PaymentGateway, method: String) { private fun applyEvent(additionalCan: Int, member: Member, paymentGateway: PaymentGateway, method: String) {
val eventCharge = Charge(0, additionalCan, status = ChargeStatus.EVENT) val eventCharge = Charge(0, additionalCan, status = ChargeStatus.EVENT)
eventCharge.title = "$additionalCan" eventCharge.title = formatMessage("can.charge.title", additionalCan)
eventCharge.member = member eventCharge.member = member
val payment = Payment( val payment = Payment(
@@ -127,4 +141,9 @@ class ChargeEventService(
else -> member.charge(0, additionalCan, "pg") 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? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) { if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException(messageKey = "common.error.bad_credentials")
} }
ApiResponse.ok(service.charge(member, request)) 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.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.moneyFormat 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.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
@@ -27,6 +29,8 @@ class ChargeTempService(
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${bootpay.hecto-application-id}") @Value("\${bootpay.hecto-application-id}")
private val bootpayApplicationId: String, private val bootpayApplicationId: String,
@@ -37,7 +41,7 @@ class ChargeTempService(
@Transactional @Transactional
fun charge(member: Member, request: ChargeTempRequest): ChargeResponse { fun charge(member: Member, request: ChargeTempRequest): ChargeResponse {
val charge = Charge(request.can, 0) val charge = Charge(request.can, 0)
charge.title = "${request.can.moneyFormat()}" charge.title = formatMessage("can.charge.title", request.can.moneyFormat())
charge.member = member charge.member = member
val payment = Payment(paymentGateway = request.paymentGateway) val payment = Payment(paymentGateway = request.paymentGateway)
@@ -52,9 +56,9 @@ class ChargeTempService(
@Transactional @Transactional
fun verify(user: User, verifyRequest: VerifyRequest) { fun verify(user: User, verifyRequest: VerifyRequest) {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException(messageKey = "can.charge.invalid_payment_info")
val member = memberRepository.findByEmail(user.username) val member = memberRepository.findByEmail(user.username)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException(messageKey = "common.error.bad_credentials")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) { if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey) val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)
@@ -72,13 +76,18 @@ class ChargeTempService(
charge.payment?.status = PaymentStatus.COMPLETE charge.payment?.status = PaymentStatus.COMPLETE
member.charge(charge.chargeCan, charge.rewardCan, "pg") member.charge(charge.chargeCan, charge.rewardCan, "pg")
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} catch (_: Exception) { } catch (_: Exception) {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException(messageKey = "can.charge.invalid_payment_info")
} }
} else { } 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.ApiResponse
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.member.Member import kr.co.vividnext.sodalive.member.Member
import org.springframework.core.io.InputStreamResource import org.springframework.core.io.InputStreamResource
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
@@ -22,14 +24,18 @@ import java.nio.charset.StandardCharsets
@RestController @RestController
@RequestMapping("/can/coupon") @RequestMapping("/can/coupon")
class CanCouponController(private val service: CanCouponService) { class CanCouponController(
private val service: CanCouponService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
@PostMapping @PostMapping
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
fun generateCoupon( fun generateCoupon(
@RequestBody request: GenerateCanCouponRequest, @RequestBody request: GenerateCanCouponRequest,
@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.generateCoupon(request)) ApiResponse.ok(service.generateCoupon(request))
} }
@@ -40,7 +46,7 @@ class CanCouponController(private val service: CanCouponService) {
@RequestBody request: ModifyCanCouponRequest, @RequestBody request: ModifyCanCouponRequest,
@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.modifyCoupon(request)) ApiResponse.ok(service.modifyCoupon(request))
} }
@@ -51,7 +57,7 @@ class CanCouponController(private val service: CanCouponService) {
@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.getCouponList(offset = pageable.offset, limit = pageable.pageSize.toLong())) 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?, @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( ApiResponse.ok(
service.getCouponNumberList( service.getCouponNumberList(
@@ -80,9 +86,11 @@ class CanCouponController(private val service: CanCouponService) {
@RequestParam couponId: Long, @RequestParam couponId: Long,
@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")
val fileName = "쿠폰번호리스트.xlsx" val fileName = messageSource
.getMessage("can.coupon.download_filename", langContext.lang)
.orEmpty()
val encodedFileName = URLEncoder.encode( val encodedFileName = URLEncoder.encode(
fileName, fileName,
StandardCharsets.UTF_8.toString() StandardCharsets.UTF_8.toString()
@@ -107,7 +115,7 @@ class CanCouponController(private val service: CanCouponService) {
@RequestBody request: UseCanCouponRequest, @RequestBody request: UseCanCouponRequest,
@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")
val completeMessage = service.useCanCoupon( val completeMessage = service.useCanCoupon(
couponNumber = request.couponNumber, couponNumber = request.couponNumber,

View File

@@ -12,7 +12,7 @@ class CanCouponIssueService(private val couponNumberRepository: CanCouponNumberR
if (!isMultipleUse(canCouponNumber)) { if (!isMultipleUse(canCouponNumber)) {
val canCouponNumberList = couponNumberRepository.findByMemberId(memberId = memberId) val canCouponNumberList = couponNumberRepository.findByMemberId(memberId = memberId)
if (canCouponNumberList.isNotEmpty()) { 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 { private fun checkCanCouponNumber(couponNumber: String): CanCouponNumber {
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber) val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.") ?: throw SodaException(messageKey = "can.coupon.invalid_number_contact")
if (canCouponNumber.member != null) { if (canCouponNumber.member != null) {
throw SodaException("이미 사용한 쿠폰번호 입니다.") throw SodaException(messageKey = "can.coupon.already_used")
} }
return canCouponNumber return canCouponNumber
@@ -34,17 +34,17 @@ class CanCouponIssueService(private val couponNumberRepository: CanCouponNumberR
private fun validateCoupon(canCoupon: CanCoupon) { private fun validateCoupon(canCoupon: CanCoupon) {
if (canCoupon.validity < LocalDateTime.now()) { if (canCoupon.validity < LocalDateTime.now()) {
throw SodaException("유효기간이 경과된 쿠폰입니다.") throw SodaException(messageKey = "can.coupon.expired")
} }
if (!canCoupon.isActive) { if (!canCoupon.isActive) {
throw SodaException("이용이 불가능한 쿠폰입니다.") throw SodaException(messageKey = "can.coupon.inactive")
} }
} }
fun checkAnyChanges(request: ModifyCanCouponRequest) { fun checkAnyChanges(request: ModifyCanCouponRequest) {
if (request.isMultipleUse == null && request.isActive == null && request.validity == null) { 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.aws.sqs.SqsEventType
import kr.co.vividnext.sodalive.can.charge.ChargeService import kr.co.vividnext.sodalive.can.charge.ChargeService
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.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
@@ -29,7 +31,9 @@ class CanCouponService(
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val applicationEventPublisher: ApplicationEventPublisher private val applicationEventPublisher: ApplicationEventPublisher,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) { ) {
fun generateCoupon(request: GenerateCanCouponRequest) { fun generateCoupon(request: GenerateCanCouponRequest) {
val message = objectMapper.writeValueAsString(request) val message = objectMapper.writeValueAsString(request)
@@ -41,7 +45,7 @@ class CanCouponService(
issueService.checkAnyChanges(request) issueService.checkAnyChanges(request)
val canCoupon = repository.findByIdOrNull(id = request.couponId) val canCoupon = repository.findByIdOrNull(id = request.couponId)
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.") ?: throw SodaException(messageKey = "can.coupon.invalid_number_contact")
if (request.validity != null) { if (request.validity != null) {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
@@ -51,7 +55,7 @@ class CanCouponService(
.toLocalDateTime() .toLocalDateTime()
if (validity <= canCoupon.validity) { if (validity <= canCoupon.validity) {
throw SodaException("유효기간은 기존 유효기간 이후 날짜로 설정하실 수 있습니다.") throw SodaException(messageKey = "can.coupon.validity_after_current")
} }
canCoupon.validity = validity canCoupon.validity = validity
@@ -85,7 +89,11 @@ class CanCouponService(
} }
fun downloadCouponNumberList(couponId: Long): ByteArrayInputStream { 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 byteArrayOutputStream = ByteArrayOutputStream()
val couponNumberList = couponNumberRepository.getAllCouponNumberList(couponId) val couponNumberList = couponNumberRepository.getAllCouponNumberList(couponId)
@@ -104,9 +112,9 @@ class CanCouponService(
couponNumberRow.createCell(1).setCellValue(insertHyphens(item.couponNumber)) couponNumberRow.createCell(1).setCellValue(insertHyphens(item.couponNumber))
couponNumberRow.createCell(2).setCellValue( couponNumberRow.createCell(2).setCellValue(
if (item.isUsed) { if (item.isUsed) {
"O" messageSource.getMessage("can.coupon.download_used_mark", langContext.lang).orEmpty()
} else { } else {
"X" messageSource.getMessage("can.coupon.download_unused_mark", langContext.lang).orEmpty()
} }
) )
} }
@@ -114,7 +122,7 @@ class CanCouponService(
workbook.write(byteArrayOutputStream) workbook.write(byteArrayOutputStream)
return ByteArrayInputStream(byteArrayOutputStream.toByteArray()) return ByteArrayInputStream(byteArrayOutputStream.toByteArray())
} catch (e: IOException) { } catch (e: IOException) {
throw SodaException("다운로드를 하지 못했습니다.\n다시 시도해 주세요.") throw SodaException(messageKey = "can.coupon.download_failed_retry")
} finally { } finally {
workbook.close() workbook.close()
byteArrayOutputStream.close() byteArrayOutputStream.close()
@@ -123,9 +131,9 @@ class CanCouponService(
fun useCanCoupon(couponNumber: String, memberId: Long): String { fun useCanCoupon(couponNumber: String, memberId: Long): String {
val member = memberRepository.findByIdOrNull(id = memberId) 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) 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.AudioContent
import kr.co.vividnext.sodalive.content.order.Order import kr.co.vividnext.sodalive.content.order.Order
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity 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.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
@@ -31,7 +33,9 @@ class CanPaymentService(
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val chargeRepository: ChargeRepository, private val chargeRepository: ChargeRepository,
private val useCanRepository: UseCanRepository, private val useCanRepository: UseCanRepository,
private val useCanCalculateRepository: UseCanCalculateRepository private val useCanCalculateRepository: UseCanCalculateRepository,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) { ) {
@Transactional @Transactional
fun spendCan( fun spendCan(
@@ -49,7 +53,7 @@ class CanPaymentService(
container: String container: String
) { ) {
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "can.payment.invalid_request_retry")
val useRewardCan = spendRewardCan(member, needCan, container) val useRewardCan = spendRewardCan(member, needCan, container)
val useChargeCan = if (needCan - useRewardCan.total > 0) { val useChargeCan = if (needCan - useRewardCan.total > 0) {
spendChargeCan(member, needCan = needCan - useRewardCan.total, container = container) spendChargeCan(member, needCan = needCan - useRewardCan.total, container = container)
@@ -58,14 +62,14 @@ class CanPaymentService(
} }
if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) { if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) {
val shortCan = needCan - useRewardCan.total - (useChargeCan?.total ?: 0)
throw SodaException( throw SodaException(
"${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " + formatMessage("can.payment.insufficient_can", shortCan)
"캔이 부족합니다. 충전 후 이용해 주세요."
) )
} }
if (!useRewardCan.verify() || useChargeCan?.verify() == false) { if (!useRewardCan.verify() || useChargeCan?.verify() == false) {
throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") throw SodaException(messageKey = "can.payment.invalid_request_retry")
} }
val useCan = UseCan( val useCan = UseCan(
@@ -121,7 +125,7 @@ class CanPaymentService(
useCan.chatRoomId = chatRoomId useCan.chatRoomId = chatRoomId
useCan.characterId = characterId useCan.characterId = characterId
} else { } else {
throw SodaException("잘못된 요청입니다.") throw SodaException(messageKey = "common.error.invalid_request")
} }
useCanRepository.save(useCan) useCanRepository.save(useCan)
@@ -306,20 +310,20 @@ class CanPaymentService(
@Transactional @Transactional
fun refund(memberId: Long, roomId: Long) { fun refund(memberId: Long, roomId: Long) {
val member = memberRepository.findByIdOrNull(memberId) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("잘못된 예약정보 입니다.") ?: throw SodaException(messageKey = "can.payment.invalid_reservation")
val useCan = repository.getCanUsedForLiveRoomNotRefund( val useCan = repository.getCanUsedForLiveRoomNotRefund(
memberId = memberId, memberId = memberId,
roomId = roomId, roomId = roomId,
canUsage = CanUsage.LIVE canUsage = CanUsage.LIVE
) ?: throw SodaException("잘못된 예약정보 입니다.") ) ?: throw SodaException(messageKey = "can.payment.invalid_reservation")
useCan.isRefund = true useCan.isRefund = true
val useCanCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) val useCanCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!)
useCanCalculates.forEach { useCanCalculates.forEach {
it.status = UseCanCalculateStatus.REFUND it.status = UseCanCalculateStatus.REFUND
val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) 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 charge.useCan = useCan
when (it.paymentGateway) { when (it.paymentGateway) {
@@ -333,7 +337,9 @@ class CanPaymentService(
status = PaymentStatus.COMPLETE, status = PaymentStatus.COMPLETE,
paymentGateway = it.paymentGateway paymentGateway = it.paymentGateway
) )
payment.method = "환불" payment.method = messageSource
.getMessage("can.payment.method.refund", langContext.lang)
.orEmpty()
charge.payment = payment charge.payment = payment
chargeRepository.save(charge) chargeRepository.save(charge)
@@ -348,7 +354,7 @@ class CanPaymentService(
container: String container: String
) { ) {
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "can.payment.invalid_request_retry")
val useRewardCan = spendRewardCan(member, needCan, container) val useRewardCan = spendRewardCan(member, needCan, container)
val useChargeCan = if (needCan - useRewardCan.total > 0) { val useChargeCan = if (needCan - useRewardCan.total > 0) {
@@ -358,14 +364,14 @@ class CanPaymentService(
} }
if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) { if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) {
val shortCan = needCan - useRewardCan.total - (useChargeCan?.total ?: 0)
throw SodaException( throw SodaException(
"${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " + formatMessage("can.payment.insufficient_can", shortCan)
"캔이 부족합니다. 충전 후 이용해 주세요."
) )
} }
if (!useRewardCan.verify() || useChargeCan?.verify() == false) { if (!useRewardCan.verify() || useChargeCan?.verify() == false) {
throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") throw SodaException(messageKey = "can.payment.invalid_request_retry")
} }
val useCan = UseCan( val useCan = UseCan(
@@ -394,7 +400,7 @@ class CanPaymentService(
container: String container: String
) { ) {
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "can.payment.invalid_request_retry")
val useRewardCan = spendRewardCan(member, needCan, container) val useRewardCan = spendRewardCan(member, needCan, container)
val useChargeCan = if (needCan - useRewardCan.total > 0) { val useChargeCan = if (needCan - useRewardCan.total > 0) {
@@ -404,14 +410,14 @@ class CanPaymentService(
} }
if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) { if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) {
val shortCan = needCan - useRewardCan.total - (useChargeCan?.total ?: 0)
throw SodaException( throw SodaException(
"${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " + formatMessage("can.payment.insufficient_can", shortCan)
"캔이 부족합니다. 충전 후 이용해 주세요."
) )
} }
if (!useRewardCan.verify() || useChargeCan?.verify() == false) { if (!useRewardCan.verify() || useChargeCan?.verify() == false) {
throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") throw SodaException(messageKey = "can.payment.invalid_request_retry")
} }
val useCan = UseCan( 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.GOOGLE_IAP)
setUseCanCalculate(null, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.APPLE_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

@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.chat.character.comment
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
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.member.Member import kr.co.vividnext.sodalive.member.Member
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
@@ -18,6 +20,8 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/api/chat/character") @RequestMapping("/api/chat/character")
class CharacterCommentController( class CharacterCommentController(
private val service: CharacterCommentService, private val service: CharacterCommentService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String private val imageHost: String
) { ) {
@@ -28,9 +32,9 @@ class CharacterCommentController(
@RequestBody request: CreateCharacterCommentRequest, @RequestBody request: CreateCharacterCommentRequest,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
if (request.comment.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") if (request.comment.isBlank()) throw SodaException(messageKey = "chat.character.comment.required")
val id = service.addComment(characterId, member, request.comment) val id = service.addComment(characterId, member, request.comment)
ApiResponse.ok(id) ApiResponse.ok(id)
@@ -43,9 +47,9 @@ class CharacterCommentController(
@RequestBody request: CreateCharacterCommentRequest, @RequestBody request: CreateCharacterCommentRequest,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
if (request.comment.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") if (request.comment.isBlank()) throw SodaException(messageKey = "chat.character.comment.required")
val id = service.addReply(characterId, commentId, member, request.comment, request.languageCode) val id = service.addReply(characterId, commentId, member, request.comment, request.languageCode)
ApiResponse.ok(id) ApiResponse.ok(id)
@@ -58,8 +62,8 @@ class CharacterCommentController(
@RequestParam(required = false) cursor: Long?, @RequestParam(required = false) cursor: Long?,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val data = service.listComments(imageHost, characterId, cursor, limit) val data = service.listComments(imageHost, characterId, cursor, limit)
ApiResponse.ok(data) ApiResponse.ok(data)
@@ -73,8 +77,8 @@ class CharacterCommentController(
@RequestParam(required = false) cursor: Long?, @RequestParam(required = false) cursor: Long?,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
// characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨 // characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨
val data = service.getReplies(imageHost, commentId, cursor, limit) val data = service.getReplies(imageHost, commentId, cursor, limit)
@@ -87,10 +91,11 @@ class CharacterCommentController(
@PathVariable commentId: Long, @PathVariable commentId: Long,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
service.deleteComment(characterId, commentId, member) service.deleteComment(characterId, commentId, member)
ApiResponse.ok(true, "댓글이 삭제되었습니다.") val message = messageSource.getMessage("chat.character.comment.deleted", langContext.lang)
ApiResponse.ok(true, message)
} }
@PostMapping("/{characterId}/comments/{commentId}/reports") @PostMapping("/{characterId}/comments/{commentId}/reports")
@@ -100,9 +105,10 @@ class CharacterCommentController(
@RequestBody request: ReportCharacterCommentRequest, @RequestBody request: ReportCharacterCommentRequest,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
service.reportComment(characterId, commentId, member, request.content) service.reportComment(characterId, commentId, member, request.content)
ApiResponse.ok(true, "신고가 접수되었습니다.") val message = messageSource.getMessage("chat.character.comment.reported", langContext.lang)
ApiResponse.ok(true, message)
} }
} }

View File

@@ -36,7 +36,7 @@ class CharacterCommentService(
entity: CharacterComment, entity: CharacterComment,
replyCountOverride: Int? = null replyCountOverride: Int? = null
): CharacterCommentResponse { ): CharacterCommentResponse {
val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.") val member = entity.member ?: throw SodaException(messageKey = "chat.character.comment.invalid")
return CharacterCommentResponse( return CharacterCommentResponse(
commentId = entity.id!!, commentId = entity.id!!,
memberId = member.id!!, memberId = member.id!!,
@@ -50,7 +50,7 @@ class CharacterCommentService(
} }
private fun toReplyResponse(imageHost: String, entity: CharacterComment): CharacterReplyResponse { private fun toReplyResponse(imageHost: String, entity: CharacterComment): CharacterReplyResponse {
val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.") val member = entity.member ?: throw SodaException(messageKey = "chat.character.comment.invalid")
return CharacterReplyResponse( return CharacterReplyResponse(
replyId = entity.id!!, replyId = entity.id!!,
memberId = member.id!!, memberId = member.id!!,
@@ -64,9 +64,10 @@ class CharacterCommentService(
@Transactional @Transactional
fun addComment(characterId: Long, member: Member, text: String, languageCode: String? = null): Long { fun addComment(characterId: Long, member: Member, text: String, languageCode: String? = null): Long {
val character = chatCharacterRepository.findById(characterId).orElseThrow { SodaException("캐릭터를 찾을 수 없습니다.") } val character = chatCharacterRepository.findById(characterId)
if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.") .orElseThrow { SodaException(messageKey = "chat.character.not_found") }
if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") if (!character.isActive) throw SodaException(messageKey = "chat.character.inactive")
if (text.isBlank()) throw SodaException(messageKey = "chat.character.comment.required")
val entity = CharacterComment(comment = text, languageCode = languageCode) val entity = CharacterComment(comment = text, languageCode = languageCode)
entity.chatCharacter = character entity.chatCharacter = character
@@ -95,12 +96,14 @@ class CharacterCommentService(
text: String, text: String,
languageCode: String? = null languageCode: String? = null
): Long { ): Long {
val character = chatCharacterRepository.findById(characterId).orElseThrow { SodaException("캐릭터를 찾을 수 없습니다.") } val character = chatCharacterRepository.findById(characterId)
if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.") .orElseThrow { SodaException(messageKey = "chat.character.not_found") }
val parent = commentRepository.findById(parentCommentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } if (!character.isActive) throw SodaException(messageKey = "chat.character.inactive")
if (parent.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.") val parent = commentRepository.findById(parentCommentId)
if (!parent.isActive) throw SodaException("비활성화된 댓글입니다.") .orElseThrow { SodaException(messageKey = "chat.character.comment.not_found") }
if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.") if (parent.chatCharacter?.id != characterId) throw SodaException(messageKey = "common.error.invalid_request")
if (!parent.isActive) throw SodaException(messageKey = "chat.character.comment.inactive")
if (text.isBlank()) throw SodaException(messageKey = "chat.character.comment.required")
val entity = CharacterComment(comment = text, languageCode = languageCode) val entity = CharacterComment(comment = text, languageCode = languageCode)
entity.chatCharacter = character entity.chatCharacter = character
@@ -162,9 +165,9 @@ class CharacterCommentService(
limit: Int = 20 limit: Int = 20
): CharacterCommentRepliesResponse { ): CharacterCommentRepliesResponse {
val original = commentRepository.findById(commentId).orElseThrow { val original = commentRepository.findById(commentId).orElseThrow {
SodaException("댓글을 찾을 수 없습니다.") SodaException(messageKey = "chat.character.comment.not_found")
} }
if (!original.isActive) throw SodaException("비활성화된 댓글입니다.") if (!original.isActive) throw SodaException(messageKey = "chat.character.comment.inactive")
val pageable = PageRequest.of(0, limit) val pageable = PageRequest.of(0, limit)
val replies = if (cursor == null) { val replies = if (cursor == null) {
@@ -207,20 +210,22 @@ class CharacterCommentService(
@Transactional @Transactional
fun deleteComment(characterId: Long, commentId: Long, member: Member) { fun deleteComment(characterId: Long, commentId: Long, member: Member) {
val comment = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } val comment = commentRepository.findById(commentId)
if (comment.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.") .orElseThrow { SodaException(messageKey = "chat.character.comment.not_found") }
if (comment.chatCharacter?.id != characterId) throw SodaException(messageKey = "common.error.invalid_request")
if (!comment.isActive) return if (!comment.isActive) return
val ownerId = comment.member?.id ?: throw SodaException("유효하지 않은 댓글입니다.") val ownerId = comment.member?.id ?: throw SodaException(messageKey = "chat.character.comment.invalid")
if (ownerId != member.id) throw SodaException("삭제 권한이 없습니다.") if (ownerId != member.id) throw SodaException(messageKey = "chat.character.comment.delete_forbidden")
comment.isActive = false comment.isActive = false
commentRepository.save(comment) commentRepository.save(comment)
} }
@Transactional @Transactional
fun reportComment(characterId: Long, commentId: Long, member: Member, content: String) { fun reportComment(characterId: Long, commentId: Long, member: Member, content: String) {
val comment = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } val comment = commentRepository.findById(commentId)
if (comment.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.") .orElseThrow { SodaException(messageKey = "chat.character.comment.not_found") }
if (content.isBlank()) throw SodaException("신고 내용을 입력해주세요.") if (comment.chatCharacter?.id != characterId) throw SodaException(messageKey = "common.error.invalid_request")
if (content.isBlank()) throw SodaException(messageKey = "chat.character.comment.report_content_required")
val report = CharacterCommentReport(content = content) val report = CharacterCommentReport(content = content)
report.comment = comment report.comment = comment

View File

@@ -155,12 +155,12 @@ class ChatCharacterController(
@PathVariable characterId: Long, @PathVariable characterId: Long,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
// 캐릭터 상세 정보 조회 // 캐릭터 상세 정보 조회
val character = service.getCharacterDetail(characterId) val character = service.getCharacterDetail(characterId)
?: throw SodaException("캐릭터를 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.character.not_found")
// 태그 가공: # prefix 규칙 적용 후 공백으로 연결 // 태그 가공: # prefix 규칙 적용 후 공백으로 연결
val tags = character.tagMappings val tags = character.tagMappings

View File

@@ -36,8 +36,8 @@ class CharacterImageController(
@RequestParam(required = false, defaultValue = "20") size: Int, @RequestParam(required = false, defaultValue = "20") size: Int,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val pageSize = if (size <= 0) 20 else minOf(size, 20) val pageSize = if (size <= 0) 20 else minOf(size, 20)
@@ -124,8 +124,8 @@ class CharacterImageController(
@RequestParam(required = false, defaultValue = "20") size: Int, @RequestParam(required = false, defaultValue = "20") size: Int,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val pageSize = if (size <= 0) 20 else minOf(size, 20) val pageSize = if (size <= 0) 20 else minOf(size, 20)
val expiration = 5L * 60L * 1000L // 5분 val expiration = 5L * 60L * 1000L // 5분
@@ -198,18 +198,18 @@ class CharacterImageController(
@RequestBody req: CharacterImagePurchaseRequest, @RequestBody req: CharacterImagePurchaseRequest,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val image = imageService.getById(req.imageId) val image = imageService.getById(req.imageId)
if (!image.isActive) throw SodaException("비활성화된 이미지입니다.") if (!image.isActive) throw SodaException(messageKey = "chat.character.image.inactive")
val isOwned = (image.imagePriceCan == 0L) || val isOwned = (image.imagePriceCan == 0L) ||
imageService.isOwnedImageByMember(image.id!!, member.id!!) imageService.isOwnedImageByMember(image.id!!, member.id!!)
if (!isOwned) { if (!isOwned) {
val needCan = image.imagePriceCan.toInt() val needCan = image.imagePriceCan.toInt()
if (needCan <= 0) throw SodaException("구매 가격이 잘못되었습니다.") if (needCan <= 0) throw SodaException(messageKey = "chat.purchase.invalid_price")
canPaymentService.spendCanForCharacterImage( canPaymentService.spendCanForCharacterImage(
memberId = member.id!!, memberId = member.id!!,

View File

@@ -64,11 +64,11 @@ class CharacterImageService(
} }
fun getById(id: Long): CharacterImage = fun getById(id: Long): CharacterImage =
imageRepository.findById(id).orElseThrow { SodaException("캐릭터 이미지를 찾을 수 없습니다: $id") } imageRepository.findById(id).orElseThrow { SodaException(messageKey = "chat.character.image.not_found") }
fun getCharacterImagePath(characterId: Long): String? { fun getCharacterImagePath(characterId: Long): String? {
val character = characterRepository.findById(characterId) val character = characterRepository.findById(characterId)
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } .orElseThrow { SodaException(messageKey = "chat.character.not_found") }
return character.imagePath return character.imagePath
} }
@@ -94,11 +94,13 @@ class CharacterImageService(
triggers: List<String> triggers: List<String>
): CharacterImage { ): CharacterImage {
val character = characterRepository.findById(characterId) val character = characterRepository.findById(characterId)
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } .orElseThrow { SodaException(messageKey = "chat.character.not_found") }
if (imagePriceCan < 0 || messagePriceCan < 0) throw SodaException("가격은 0 can 이상이어야 합니다.") if (imagePriceCan < 0 || messagePriceCan < 0) {
throw SodaException(messageKey = "chat.character.image.min_price")
}
if (!character.isActive) throw SodaException("비활성화된 캐릭터에는 이미지를 등록할 수 없습니다: $characterId") if (!character.isActive) throw SodaException(messageKey = "chat.character.inactive_image_register")
val nextOrder = (imageRepository.findMaxSortOrderByCharacterId(characterId)) + 1 val nextOrder = (imageRepository.findMaxSortOrderByCharacterId(characterId)) + 1
val entity = CharacterImage( val entity = CharacterImage(
@@ -122,7 +124,7 @@ class CharacterImageService(
@Transactional @Transactional
fun updateTriggers(imageId: Long, triggers: List<String>): CharacterImage { fun updateTriggers(imageId: Long, triggers: List<String>): CharacterImage {
val image = getById(imageId) val image = getById(imageId)
if (!image.isActive) throw SodaException("비활성화된 이미지는 수정할 수 없습니다: $imageId") if (!image.isActive) throw SodaException(messageKey = "chat.character.image.inactive_update")
applyTriggers(image, triggers) applyTriggers(image, triggers)
return image return image
} }
@@ -159,8 +161,10 @@ class CharacterImageService(
val updated = mutableListOf<CharacterImage>() val updated = mutableListOf<CharacterImage>()
ids.forEachIndexed { idx, id -> ids.forEachIndexed { idx, id ->
val img = getById(id) val img = getById(id)
if (img.chatCharacter.id != characterId) throw SodaException("다른 캐릭터의 이미지가 포함되어 있습니다: $id") if (img.chatCharacter.id != characterId) {
if (!img.isActive) throw SodaException("비활성화된 이미지는 순서를 변경할 수 없습니다: $id") throw SodaException(messageKey = "chat.character.image.other_character_included")
}
if (!img.isActive) throw SodaException(messageKey = "chat.character.image.inactive_order_change")
img.sortOrder = idx + 1 img.sortOrder = idx + 1
updated.add(img) updated.add(img)
} }

View File

@@ -26,7 +26,7 @@ class ChatCharacterBannerService(
*/ */
fun getBannerById(bannerId: Long): ChatCharacterBanner { fun getBannerById(bannerId: Long): ChatCharacterBanner {
return bannerRepository.findById(bannerId) return bannerRepository.findById(bannerId)
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") }
} }
/** /**
@@ -39,10 +39,10 @@ class ChatCharacterBannerService(
@Transactional @Transactional
fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner { fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner {
val character = characterRepository.findById(characterId) val character = characterRepository.findById(characterId)
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } .orElseThrow { SodaException(messageKey = "chat.character.not_found") }
if (!character.isActive) { if (!character.isActive) {
throw SodaException("비활성화된 캐릭터에는 배너를 등록할 수 없습니다: $characterId") throw SodaException(messageKey = "chat.character.inactive_banner_register")
} }
// 정렬 순서가 지정되지 않은 경우 가장 마지막 순서로 설정 // 정렬 순서가 지정되지 않은 경우 가장 마지막 순서로 설정
@@ -68,10 +68,10 @@ class ChatCharacterBannerService(
@Transactional @Transactional
fun updateBanner(bannerId: Long, imagePath: String? = null, characterId: Long? = null): ChatCharacterBanner { fun updateBanner(bannerId: Long, imagePath: String? = null, characterId: Long? = null): ChatCharacterBanner {
val banner = bannerRepository.findById(bannerId) val banner = bannerRepository.findById(bannerId)
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") }
if (!banner.isActive) { if (!banner.isActive) {
throw SodaException("비활성화된 배너는 수정할 수 없습니다: $bannerId") throw SodaException(messageKey = "chat.character.banner.inactive_update")
} }
// 이미지 경로 변경 // 이미지 경로 변경
@@ -82,10 +82,10 @@ class ChatCharacterBannerService(
// 캐릭터 변경 // 캐릭터 변경
if (characterId != null) { if (characterId != null) {
val character = characterRepository.findById(characterId) val character = characterRepository.findById(characterId)
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") } .orElseThrow { SodaException(messageKey = "chat.character.not_found") }
if (!character.isActive) { if (!character.isActive) {
throw SodaException("비활성화된 캐릭터로는 변경할 수 없습니다: $characterId") throw SodaException(messageKey = "chat.character.inactive_banner_change")
} }
banner.chatCharacter = character banner.chatCharacter = character
@@ -100,7 +100,7 @@ class ChatCharacterBannerService(
@Transactional @Transactional
fun deleteBanner(bannerId: Long) { fun deleteBanner(bannerId: Long) {
val banner = bannerRepository.findById(bannerId) val banner = bannerRepository.findById(bannerId)
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") }
banner.isActive = false banner.isActive = false
bannerRepository.save(banner) bannerRepository.save(banner)
@@ -119,10 +119,10 @@ class ChatCharacterBannerService(
for (index in ids.indices) { for (index in ids.indices) {
val banner = bannerRepository.findById(ids[index]) val banner = bannerRepository.findById(ids[index])
.orElseThrow { SodaException("배너를 찾을 수 없습니다: ${ids[index]}") } .orElseThrow { SodaException(messageKey = "chat.character.banner.not_found") }
if (!banner.isActive) { if (!banner.isActive) {
throw SodaException("비활성화된 배너는 수정할 수 없습니다: ${ids[index]}") throw SodaException(messageKey = "chat.character.banner.inactive_update")
} }
banner.sortOrder = index + 1 banner.sortOrder = index + 1

View File

@@ -702,7 +702,7 @@ class ChatCharacterService(
): ChatCharacter { ): ChatCharacter {
// 캐릭터 조회 // 캐릭터 조회
val chatCharacter = findById(request.id) val chatCharacter = findById(request.id)
?: throw kr.co.vividnext.sodalive.common.SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}") ?: throw kr.co.vividnext.sodalive.common.SodaException(messageKey = "chat.character.not_found")
// isActive가 false이면 isActive = false, name = "inactive_$name"으로 변경하고 나머지는 반영하지 않는다. // isActive가 false이면 isActive = false, name = "inactive_$name"으로 변경하고 나머지는 반영하지 않는다.
if (request.isActive != null && !request.isActive) { if (request.isActive != null && !request.isActive) {

View File

@@ -126,8 +126,8 @@ class OriginalWorkController(
@PathVariable id: Long, @PathVariable id: Long,
@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")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val ow = queryService.getOriginalWork(id) val ow = queryService.getOriginalWork(id)
val chars = queryService.getActiveCharactersPage(id, page = 0, size = 20).content val chars = queryService.getActiveCharactersPage(id, page = 0, size = 20).content

View File

@@ -44,7 +44,7 @@ class OriginalWorkQueryService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getOriginalWork(id: Long): OriginalWork { fun getOriginalWork(id: Long): OriginalWork {
return originalWorkRepository.findByIdAndIsDeletedFalse(id) return originalWorkRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "chat.original.not_found") }
} }
/** /**
@@ -54,7 +54,7 @@ class OriginalWorkQueryService(
fun getActiveCharactersPage(originalWorkId: Long, page: Int = 0, size: Int = 20): Page<ChatCharacter> { fun getActiveCharactersPage(originalWorkId: Long, page: Int = 0, size: Int = 20): Page<ChatCharacter> {
// 원작 존재 및 소프트 삭제 여부 확인 // 원작 존재 및 소프트 삭제 여부 확인
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId) originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") } .orElseThrow { SodaException(messageKey = "chat.original.not_found") }
val safePage = if (page < 0) 0 else page val safePage = if (page < 0) 0 else page
val safeSize = when { val safeSize = when {

View File

@@ -32,8 +32,8 @@ class ChatQuotaController(
fun getMyQuota( fun getMyQuota(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
): ApiResponse<ChatQuotaStatusResponse> = run { ): ApiResponse<ChatQuotaStatusResponse> = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val s = chatQuotaService.getStatus(member.id!!) val s = chatQuotaService.getStatus(member.id!!)
ApiResponse.ok(ChatQuotaStatusResponse(s.totalRemaining, s.nextRechargeAtEpochMillis)) ApiResponse.ok(ChatQuotaStatusResponse(s.totalRemaining, s.nextRechargeAtEpochMillis))
@@ -44,9 +44,9 @@ class ChatQuotaController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
@RequestBody request: ChatQuotaPurchaseRequest @RequestBody request: ChatQuotaPurchaseRequest
): ApiResponse<ChatQuotaStatusResponse> = run { ): ApiResponse<ChatQuotaStatusResponse> = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
if (request.container.isBlank()) throw SodaException("container를 확인해주세요.") if (request.container.isBlank()) throw SodaException(messageKey = "chat.quota.container_required")
// 30캔 차감 처리 (결제 기록 남김) // 30캔 차감 처리 (결제 기록 남김)
canPaymentService.spendCan( canPaymentService.spendCan(

View File

@@ -52,27 +52,27 @@ class ChatRoomQuotaController(
@PathVariable chatRoomId: Long, @PathVariable chatRoomId: Long,
@RequestBody req: PurchaseRoomQuotaRequest @RequestBody req: PurchaseRoomQuotaRequest
): ApiResponse<PurchaseRoomQuotaResponse> = run { ): ApiResponse<PurchaseRoomQuotaResponse> = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
if (req.container.isBlank()) throw SodaException("잘못된 접근입니다") if (req.container.isBlank()) throw SodaException(messageKey = "chat.room.quota.invalid_access")
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
// 내 참여 여부 확인 // 내 참여 여부 확인
participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "chat.room.quota.invalid_access")
// 캐릭터 참여자 확인(유효한 AI 캐릭터 방인지 체크 및 characterId 기본값 보조) // 캐릭터 참여자 확인(유효한 AI 캐릭터 방인지 체크 및 characterId 기본값 보조)
val characterParticipant = participantRepository val characterParticipant = participantRepository
.findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER) .findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER)
?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room")
val character = characterParticipant.character val character = characterParticipant.character
?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room")
val characterId = character.id val characterId = character.id
?: throw SodaException("잘못된 요청입니다. 캐릭터 정보를 확인해주세요.") ?: throw SodaException(messageKey = "chat.room.quota.character_required")
// 서비스에서 결제 포함하여 처리 // 서비스에서 결제 포함하여 처리
val status = chatRoomQuotaService.purchase( val status = chatRoomQuotaService.purchase(
@@ -98,20 +98,20 @@ class ChatRoomQuotaController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
@PathVariable chatRoomId: Long @PathVariable chatRoomId: Long
): ApiResponse<RoomQuotaStatusResponse> = run { ): ApiResponse<RoomQuotaStatusResponse> = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
// 내 참여 여부 확인 // 내 참여 여부 확인
participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "chat.room.quota.invalid_access")
// 캐릭터 확인 // 캐릭터 확인
val characterParticipant = participantRepository val characterParticipant = participantRepository
.findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER) .findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER)
?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room")
val character = characterParticipant.character val character = characterParticipant.character
?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") ?: throw SodaException(messageKey = "chat.room.quota.not_ai_room")
// 글로벌 Lazy refill // 글로벌 Lazy refill
val globalStatus = chatQuotaService.getStatus(member.id!!) val globalStatus = chatQuotaService.getStatus(member.id!!)

View File

@@ -75,7 +75,7 @@ class ChatRoomQuotaService(
val now = Instant.now() val now = Instant.now()
val nowMillis = now.toEpochMilli() val nowMillis = now.toEpochMilli()
val quota = repo.findForUpdate(memberId, chatRoomId) val quota = repo.findForUpdate(memberId, chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
// 충전 시간이 지났다면 무료 10으로 리셋하고 next=null // 충전 시간이 지났다면 무료 10으로 리셋하고 next=null
if (quota.nextRechargeAt != null && quota.nextRechargeAt!! <= nowMillis) { if (quota.nextRechargeAt != null && quota.nextRechargeAt!! <= nowMillis) {
@@ -98,7 +98,7 @@ class ChatRoomQuotaService(
val globalFree = globalFreeProvider() val globalFree = globalFreeProvider()
if (globalFree <= 0) { if (globalFree <= 0) {
// 전송 차단: 글로벌 무료가 0이며 유료도 0 → 전송 불가 // 전송 차단: 글로벌 무료가 0이며 유료도 0 → 전송 불가
throw SodaException("오늘의 무료 채팅이 모두 소진되었습니다. 내일 다시 이용해 주세요.") throw SodaException(messageKey = "chat.room.quota.global_free_exhausted")
} }
if (quota.remainingFree <= 0) { if (quota.remainingFree <= 0) {
// 전송 차단: 룸 무료가 0이며 유료도 0 → 전송 불가 // 전송 차단: 룸 무료가 0이며 유료도 0 → 전송 불가
@@ -107,7 +107,7 @@ class ChatRoomQuotaService(
quota.nextRechargeAt = now.plus(Duration.ofHours(6)).toEpochMilli() quota.nextRechargeAt = now.plus(Duration.ofHours(6)).toEpochMilli()
} }
throw SodaException("무료 채팅이 모두 소진되었습니다.") throw SodaException(messageKey = "chat.room.quota.room_free_exhausted")
} }
// 둘 다 가능 → 차감 // 둘 다 가능 → 차감

View File

@@ -42,8 +42,8 @@ class ChatRoomController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
@RequestBody request: CreateChatRoomRequest @RequestBody request: CreateChatRoomRequest
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val response = chatRoomService.createOrGetChatRoom(member, request.characterId) val response = chatRoomService.createOrGetChatRoom(member, request.characterId)
ApiResponse.ok(response) ApiResponse.ok(response)
@@ -77,8 +77,8 @@ class ChatRoomController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
@PathVariable chatRoomId: Long @PathVariable chatRoomId: Long
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val isActive = chatRoomService.isMyRoomSessionActive(member, chatRoomId) val isActive = chatRoomService.isMyRoomSessionActive(member, chatRoomId)
ApiResponse.ok(isActive) ApiResponse.ok(isActive)
@@ -95,8 +95,8 @@ class ChatRoomController(
@PathVariable chatRoomId: Long, @PathVariable chatRoomId: Long,
@RequestParam(required = false) characterImageId: Long? @RequestParam(required = false) characterImageId: Long?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val response = chatRoomService.enterChatRoom(member, chatRoomId, characterImageId) val response = chatRoomService.enterChatRoom(member, chatRoomId, characterImageId)
ApiResponse.ok(response) ApiResponse.ok(response)
@@ -114,8 +114,8 @@ class ChatRoomController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
@PathVariable chatRoomId: Long @PathVariable chatRoomId: Long
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
chatRoomService.leaveChatRoom(member, chatRoomId) chatRoomService.leaveChatRoom(member, chatRoomId)
ApiResponse.ok(true) ApiResponse.ok(true)
@@ -134,8 +134,8 @@ class ChatRoomController(
@RequestParam(defaultValue = "20") limit: Int, @RequestParam(defaultValue = "20") limit: Int,
@RequestParam(required = false) cursor: Long? @RequestParam(required = false) cursor: Long?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val response = chatRoomService.getChatMessages(member, chatRoomId, cursor, limit) val response = chatRoomService.getChatMessages(member, chatRoomId, cursor, limit)
ApiResponse.ok(response) ApiResponse.ok(response)
@@ -153,8 +153,8 @@ class ChatRoomController(
@PathVariable chatRoomId: Long, @PathVariable chatRoomId: Long,
@RequestBody request: SendChatMessageRequest @RequestBody request: SendChatMessageRequest
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
if (request.message.isBlank()) { if (request.message.isBlank()) {
ApiResponse.error() ApiResponse.error()
@@ -176,8 +176,8 @@ class ChatRoomController(
@PathVariable messageId: Long, @PathVariable messageId: Long,
@RequestBody request: ChatMessagePurchaseRequest @RequestBody request: ChatMessagePurchaseRequest
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val result = chatRoomService.purchaseMessage(member, chatRoomId, messageId, request.container) val result = chatRoomService.purchaseMessage(member, chatRoomId, messageId, request.container)
ApiResponse.ok(result) ApiResponse.ok(result)
@@ -195,8 +195,8 @@ class ChatRoomController(
@PathVariable chatRoomId: Long, @PathVariable chatRoomId: Long,
@RequestBody request: ChatRoomResetRequest @RequestBody request: ChatRoomResetRequest
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException(messageKey = "common.error.adult_verification_required")
val response = chatRoomService.resetChatRoom(member, chatRoomId, request.container) val response = chatRoomService.resetChatRoom(member, chatRoomId, request.container)
ApiResponse.ok(response) ApiResponse.ok(response)

View File

@@ -29,6 +29,7 @@ import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext 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.Member
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
@@ -54,6 +55,7 @@ class ChatRoomService(
private val characterService: ChatCharacterService, private val characterService: ChatCharacterService,
private val characterImageService: CharacterImageService, private val characterImageService: CharacterImageService,
private val langContext: LangContext, private val langContext: LangContext,
private val messageSource: SodaMessageSource,
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository, private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
private val canPaymentService: kr.co.vividnext.sodalive.can.payment.CanPaymentService, private val canPaymentService: kr.co.vividnext.sodalive.can.payment.CanPaymentService,
private val imageCloudFront: kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront, private val imageCloudFront: kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront,
@@ -77,19 +79,19 @@ class ChatRoomService(
@Transactional @Transactional
fun purchaseMessage(member: Member, chatRoomId: Long, messageId: Long, container: String): ChatMessageItemDto { fun purchaseMessage(member: Member, chatRoomId: Long, messageId: Long, container: String): ChatMessageItemDto {
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
// 참여 여부 검증 // 참여 여부 검증
participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "chat.room.invalid_access")
val message = messageRepository.findById(messageId).orElseThrow { val message = messageRepository.findById(messageId).orElseThrow {
SodaException("메시지를 찾을 수 없습니다.") SodaException(messageKey = "chat.message.not_found")
} }
if (!message.isActive) throw SodaException("비활성화된 메시지입니다.") if (!message.isActive) throw SodaException(messageKey = "chat.message.inactive")
if (message.chatRoom.id != room.id) throw SodaException("잘못된 접근입니다") if (message.chatRoom.id != room.id) throw SodaException(messageKey = "chat.room.invalid_access")
val price = message.price ?: throw SodaException("구매할 수 없는 메시지입니다.") val price = message.price ?: throw SodaException(messageKey = "chat.message.not_purchasable")
if (price <= 0) throw SodaException("구매 가격이 잘못되었습니다.") if (price <= 0) throw SodaException(messageKey = "chat.purchase.invalid_price")
// 이미지 메시지인 경우: 이미 소유했다면 결제 생략하고 DTO 반환 // 이미지 메시지인 경우: 이미 소유했다면 결제 생략하고 DTO 반환
if (message.messageType == ChatMessageType.IMAGE) { if (message.messageType == ChatMessageType.IMAGE) {
@@ -124,7 +126,7 @@ class ChatRoomService(
fun createOrGetChatRoom(member: Member, characterId: Long): CreateChatRoomResponse { fun createOrGetChatRoom(member: Member, characterId: Long): CreateChatRoomResponse {
// 1. 캐릭터 조회 // 1. 캐릭터 조회
val character = characterService.findById(characterId) val character = characterService.findById(characterId)
?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: $characterId") ?: throw SodaException(messageKey = "chat.room.character_not_found")
// 2. 이미 참여 중인 채팅방이 있는지 확인 // 2. 이미 참여 중인 채팅방이 있는지 확인
val existingChatRoom = chatRoomRepository.findActiveChatRoomByMemberAndCharacter(member, character) val existingChatRoom = chatRoomRepository.findActiveChatRoomByMemberAndCharacter(member, character)
@@ -225,21 +227,21 @@ class ChatRoomService(
// success가 false이면 throw // success가 false이면 throw
if (!apiResponse.success) { if (!apiResponse.success) {
throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "chat.room.create_failed_retry")
} }
// success가 true이면 파라미터로 넘긴 값과 일치하는지 확인 // success가 true이면 파라미터로 넘긴 값과 일치하는지 확인
val data = apiResponse.data ?: throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") val data = apiResponse.data ?: throw SodaException(messageKey = "chat.room.create_failed_retry")
if (data.userId != userId && data.character.id != characterUUID && data.status != "active") { if (data.userId != userId && data.character.id != characterUUID && data.status != "active") {
throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "chat.room.create_failed_retry")
} }
// 세션 ID 반환 // 세션 ID 반환
return data.sessionId return data.sessionId
} catch (e: Exception) { } catch (e: Exception) {
log.error(e.message) log.error(e.message)
throw SodaException("채팅방 생성에 실패했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "chat.room.create_failed_retry")
} }
} }
@@ -264,7 +266,7 @@ class ChatRoomService(
} }
} else { } else {
if (latest?.message.isNullOrBlank() && latest?.characterImage != null) { if (latest?.message.isNullOrBlank() && latest?.characterImage != null) {
"[이미지]" messageSource.getMessage("chat.room.last_message_image", langContext.lang).orEmpty()
} else { } else {
"" ""
} }
@@ -304,11 +306,19 @@ class ChatRoomService(
val now = LocalDateTime.now() val now = LocalDateTime.now()
val duration = Duration.between(time, now) val duration = Duration.between(time, now)
val seconds = duration.seconds val seconds = duration.seconds
if (seconds <= 60) return "방금" if (seconds <= 60) {
return messageSource.getMessage("chat.room.time.just_now", langContext.lang).orEmpty()
}
val minutes = duration.toMinutes() val minutes = duration.toMinutes()
if (minutes < 60) return "${minutes}분 전" if (minutes < 60) {
val template = messageSource.getMessage("chat.room.time.minutes_ago", langContext.lang).orEmpty()
return String.format(template, minutes)
}
val hours = duration.toHours() val hours = duration.toHours()
if (hours < 24) return "${hours}시간 전" if (hours < 24) {
val template = messageSource.getMessage("chat.room.time.hours_ago", langContext.lang).orEmpty()
return String.format(template, hours)
}
// 그 외: 날짜 (yyyy-MM-dd) // 그 외: 날짜 (yyyy-MM-dd)
return time.toLocalDate().toString() return time.toLocalDate().toString()
} }
@@ -316,11 +326,9 @@ class ChatRoomService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun isMyRoomSessionActive(member: Member, chatRoomId: Long): Boolean { fun isMyRoomSessionActive(member: Member, chatRoomId: Long): Boolean {
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
val participant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
if (participant == null) { ?: throw SodaException(messageKey = "common.error.access_denied")
throw SodaException("잘못된 접근입니다")
}
return fetchSessionActive(room.sessionId) return fetchSessionActive(room.sessionId)
} }
@@ -328,7 +336,7 @@ class ChatRoomService(
fun enterChatRoom(member: Member, chatRoomId: Long, characterImageId: Long? = null): ChatRoomEnterResponse { fun enterChatRoom(member: Member, chatRoomId: Long, characterImageId: Long? = null): ChatRoomEnterResponse {
// 1) 활성 여부 무관하게 방 조회 // 1) 활성 여부 무관하게 방 조회
val baseRoom = chatRoomRepository.findById(chatRoomId).orElseThrow { val baseRoom = chatRoomRepository.findById(chatRoomId).orElseThrow {
SodaException("채팅방을 찾을 수 없습니다.") SodaException(messageKey = "chat.error.room_not_found")
} }
// 2) 기본 방 기준 참여/활성 여부 확인 // 2) 기본 방 기준 참여/활성 여부 확인
@@ -342,10 +350,10 @@ class ChatRoomService(
ParticipantType.CHARACTER ParticipantType.CHARACTER
) ?: baseRoom.participants.firstOrNull { ) ?: baseRoom.participants.firstOrNull {
it.participantType == ParticipantType.CHARACTER it.participantType == ParticipantType.CHARACTER
} ?: throw SodaException("잘못된 접근입니다") } ?: throw SodaException(messageKey = "common.error.invalid_request")
val baseCharacter = baseCharacterParticipant.character val baseCharacter = baseCharacterParticipant.character
?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") ?: throw SodaException(messageKey = "common.error.unknown")
// 4) 유효한 입장 대상 방 결정 // 4) 유효한 입장 대상 방 결정
val effectiveRoom: ChatRoom = if (isActiveRoom && isMyActiveParticipation) { val effectiveRoom: ChatRoom = if (isActiveRoom && isMyActiveParticipation) {
@@ -355,9 +363,9 @@ class ChatRoomService(
val alt = chatRoomRepository.findActiveChatRoomByMemberAndCharacter(member, baseCharacter) val alt = chatRoomRepository.findActiveChatRoomByMemberAndCharacter(member, baseCharacter)
alt ?: ( // 대체 방이 없으면 기존과 동일하게 예외 처리 alt ?: ( // 대체 방이 없으면 기존과 동일하게 예외 처리
if (!isActiveRoom) { if (!isActiveRoom) {
throw SodaException("채팅방을 찾을 수 없습니다.") throw SodaException(messageKey = "chat.error.room_not_found")
} else { } else {
throw SodaException("잘못된 접근입니다") throw SodaException(messageKey = "common.error.invalid_request")
} }
) )
} }
@@ -368,10 +376,10 @@ class ChatRoomService(
ParticipantType.CHARACTER ParticipantType.CHARACTER
) ?: effectiveRoom.participants.firstOrNull { ) ?: effectiveRoom.participants.firstOrNull {
it.participantType == ParticipantType.CHARACTER it.participantType == ParticipantType.CHARACTER
} ?: throw SodaException("잘못된 접근입니다") } ?: throw SodaException(messageKey = "common.error.invalid_request")
val character = characterParticipant.character val character = characterParticipant.character
?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") ?: throw SodaException(messageKey = "common.error.unknown")
val imageUrl = "$imageHost/${character.imagePath ?: "profile/default-profile.png"}" val imageUrl = "$imageHost/${character.imagePath ?: "profile/default-profile.png"}"
val characterDto = ChatRoomEnterCharacterDto( val characterDto = ChatRoomEnterCharacterDto(
@@ -512,23 +520,23 @@ class ChatRoomService(
// success가 false이면 throw // success가 false이면 throw
if (!apiResponse.success) { if (!apiResponse.success) {
throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "chat.error.retry")
} }
val status = apiResponse.data?.status val status = apiResponse.data?.status
return status == "active" return status == "active"
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") throw SodaException(messageKey = "chat.error.retry")
} }
} }
@Transactional @Transactional
fun leaveChatRoom(member: Member, chatRoomId: Long, throwOnSessionEndFailure: Boolean = false) { fun leaveChatRoom(member: Member, chatRoomId: Long, throwOnSessionEndFailure: Boolean = false) {
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
val participant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) val participant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "chat.room.invalid_access")
// 1) 나가기 처리 // 1) 나가기 처리
participant.isActive = false participant.isActive = false
@@ -591,10 +599,9 @@ class ChatRoomService(
} }
} }
// 최종 실패 처리 // 최종 실패 처리
val message = "채팅방 세션 종료에 실패했습니다. 다시 시도해 주세요."
if (throwOnFailure) { if (throwOnFailure) {
log.error("[chat] 외부 세션 종료 최종 실패(예외 전파): sessionId={}, attempts={}", sessionId, maxAttempts) log.error("[chat] 외부 세션 종료 최종 실패(예외 전파): sessionId={}, attempts={}", sessionId, maxAttempts)
throw SodaException(message) throw SodaException(messageKey = "chat.room.session_end_failed")
} else { } else {
log.error("[chat] 외부 세션 종료 최종 실패: sessionId={}, attempts={}", sessionId, maxAttempts) log.error("[chat] 외부 세션 종료 최종 실패: sessionId={}, attempts={}", sessionId, maxAttempts)
} }
@@ -603,9 +610,9 @@ class ChatRoomService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getChatMessages(member: Member, chatRoomId: Long, cursor: Long?, limit: Int = 20): ChatMessagesPageResponse { fun getChatMessages(member: Member, chatRoomId: Long, cursor: Long?, limit: Int = 20): ChatMessagesPageResponse {
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "chat.room.invalid_access")
val pageable = PageRequest.of(0, limit) val pageable = PageRequest.of(0, limit)
val fetched = if (cursor != null) { val fetched = if (cursor != null) {
@@ -638,18 +645,18 @@ class ChatRoomService(
fun sendMessage(member: Member, chatRoomId: Long, message: String): SendChatMessageResponse { fun sendMessage(member: Member, chatRoomId: Long, message: String): SendChatMessageResponse {
// 1) 방 존재 확인 // 1) 방 존재 확인
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
// 2) 참여 여부 확인 (USER) // 2) 참여 여부 확인 (USER)
val myParticipant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) val myParticipant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "chat.room.invalid_access")
// 3) 캐릭터 참여자 조회 // 3) 캐릭터 참여자 조회
val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue( val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue(
room, room,
ParticipantType.CHARACTER ParticipantType.CHARACTER
) ?: throw SodaException("잘못된 접근입니다") ) ?: throw SodaException(messageKey = "chat.room.invalid_access")
val character = characterParticipant.character val character = characterParticipant.character
?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") ?: throw SodaException(messageKey = "chat.error.retry")
// 4) 외부 API 호출 준비 // 4) 외부 API 호출 준비
val userId = generateUserId(member.id!!) val userId = generateUserId(member.id!!)
@@ -835,7 +842,7 @@ class ChatRoomService(
} }
} }
log.error("[chat] 외부 채팅 전송 최종 실패 attempts={}", maxAttempts) log.error("[chat] 외부 채팅 전송 최종 실패 attempts={}", maxAttempts)
throw SodaException("메시지 전송을 실패했습니다.") throw SodaException(messageKey = "chat.message.send_failed")
} }
private fun callExternalApiForChatSend( private fun callExternalApiForChatSend(
@@ -877,12 +884,12 @@ class ChatRoomService(
) )
if (!apiResponse.success) { if (!apiResponse.success) {
throw SodaException("메시지 전송을 실패했습니다.") throw SodaException(messageKey = "chat.message.send_failed")
} }
val data = apiResponse.data ?: throw SodaException("메시지 전송을 실패했습니다.") val data = apiResponse.data ?: throw SodaException(messageKey = "chat.message.send_failed")
val characterContent = data.characterResponse.content val characterContent = data.characterResponse.content
if (characterContent.isBlank()) { if (characterContent.isBlank()) {
throw SodaException("메시지 전송을 실패했습니다.") throw SodaException(messageKey = "chat.message.send_failed")
} }
return characterContent return characterContent
} }
@@ -905,16 +912,16 @@ class ChatRoomService(
fun resetChatRoom(member: Member, chatRoomId: Long, container: String): CreateChatRoomResponse { fun resetChatRoom(member: Member, chatRoomId: Long, container: String): CreateChatRoomResponse {
// 0) 방 존재 및 내 참여 여부 확인 // 0) 방 존재 및 내 참여 여부 확인
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId)
?: throw SodaException("채팅방을 찾을 수 없습니다.") ?: throw SodaException(messageKey = "chat.error.room_not_found")
participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
?: throw SodaException("잘못된 접근입니다") ?: throw SodaException(messageKey = "chat.room.invalid_access")
// 1) AI 캐릭터 채팅방인지 확인 (CHARACTER 타입의 활성 참여자 존재 확인) // 1) AI 캐릭터 채팅방인지 확인 (CHARACTER 타입의 활성 참여자 존재 확인)
val characterParticipant = participantRepository val characterParticipant = participantRepository
.findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER) .findByChatRoomAndParticipantTypeAndIsActiveTrue(room, ParticipantType.CHARACTER)
?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") ?: throw SodaException(messageKey = "chat.room.not_ai_room")
val character = characterParticipant.character val character = characterParticipant.character
?: throw SodaException("AI 캐릭터 채팅방이 아닙니다.") ?: throw SodaException(messageKey = "chat.room.not_ai_room")
// 2) 30캔 결제 (채팅방 초기화 전용 CanUsage 사용) // 2) 30캔 결제 (채팅방 초기화 전용 CanUsage 사용)
canPaymentService.spendCan( canPaymentService.spendCan(

View File

@@ -1,5 +1,12 @@
package kr.co.vividnext.sodalive.common package kr.co.vividnext.sodalive.common
class SodaException(message: String, val errorProperty: String? = null) : RuntimeException(message) class SodaException(
message: String? = null,
val errorProperty: String? = null,
val messageKey: String? = null
) : RuntimeException(message)
class AdsChargeException(message: String) : RuntimeException(message) class AdsChargeException(
message: String? = null,
val messageKey: String? = null
) : RuntimeException(message)

View File

@@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.common package kr.co.vividnext.sodalive.common
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.dao.DataIntegrityViolationException import org.springframework.dao.DataIntegrityViolationException
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
@@ -13,14 +15,20 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@RestControllerAdvice @RestControllerAdvice
class SodaExceptionHandler { class SodaExceptionHandler(
private val langContext: LangContext,
private val messageSource: SodaMessageSource
) {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@ExceptionHandler(SodaException::class) @ExceptionHandler(SodaException::class)
fun handleSodaException(e: SodaException) = run { fun handleSodaException(e: SodaException) = run {
logger.error("API error", e) logger.error("API error", e)
val message = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, langContext.lang) }
?: e.message?.takeIf { it.isNotBlank() }
?: messageSource.getMessage("common.error.unknown", langContext.lang)
ApiResponse.error( ApiResponse.error(
message = e.message, message = message,
errorProperty = e.errorProperty errorProperty = e.errorProperty
) )
} }
@@ -28,44 +36,53 @@ class SodaExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException::class) @ExceptionHandler(MaxUploadSizeExceededException::class)
fun handleMaxUploadSizeExceededException(e: MaxUploadSizeExceededException) = run { fun handleMaxUploadSizeExceededException(e: MaxUploadSizeExceededException) = run {
logger.error("API error", e) logger.error("API error", e)
ApiResponse.error(message = "파일용량은 최대 1024MB까지 저장할 수 있습니다.") val message = messageSource.getMessage("common.error.max_upload_size", langContext.lang)
ApiResponse.error(message = message)
} }
@ExceptionHandler(AccessDeniedException::class) @ExceptionHandler(AccessDeniedException::class)
fun handleAccessDeniedException(e: AccessDeniedException) = run { fun handleAccessDeniedException(e: AccessDeniedException) = run {
logger.error("API error", e) logger.error("API error", e)
ApiResponse.error(message = "권한이 없습니다.") val message = messageSource.getMessage("common.error.access_denied", langContext.lang)
ApiResponse.error(message = message)
} }
@ExceptionHandler(InternalAuthenticationServiceException::class) @ExceptionHandler(InternalAuthenticationServiceException::class)
fun handleInternalAuthenticationServiceException(e: InternalAuthenticationServiceException) = run { fun handleInternalAuthenticationServiceException(e: InternalAuthenticationServiceException) = run {
logger.error("API error", e) logger.error("API error", e)
ApiResponse.error("로그인 정보를 확인해주세요.") val message = messageSource.getMessage("common.error.bad_credentials", langContext.lang)
ApiResponse.error(message)
} }
@ExceptionHandler(BadCredentialsException::class) @ExceptionHandler(BadCredentialsException::class)
fun handleBadCredentialsException(e: BadCredentialsException) = run { fun handleBadCredentialsException(e: BadCredentialsException) = run {
logger.error("API error", e) logger.error("API error", e)
ApiResponse.error("로그인 정보를 확인해주세요.") val message = messageSource.getMessage("common.error.bad_credentials", langContext.lang)
ApiResponse.error(message)
} }
@ExceptionHandler(DataIntegrityViolationException::class) @ExceptionHandler(DataIntegrityViolationException::class)
fun handleDataIntegrityViolationException(e: DataIntegrityViolationException) = run { fun handleDataIntegrityViolationException(e: DataIntegrityViolationException) = run {
logger.error("API error", e) logger.error("API error", e)
ApiResponse.error("이미 등록되어 있습니다.") val message = messageSource.getMessage("common.error.already_registered", langContext.lang)
ApiResponse.error(message)
} }
@ResponseStatus(value = HttpStatus.NOT_FOUND) @ResponseStatus(value = HttpStatus.NOT_FOUND)
@ExceptionHandler(AdsChargeException::class) @ExceptionHandler(AdsChargeException::class)
fun handleAdsChargeException(e: AdsChargeException) = run { fun handleAdsChargeException(e: AdsChargeException) = run {
logger.error("API error - AdsChargeException ::: ", e) logger.error("API error - AdsChargeException ::: ", e)
ApiResponse.error("잘못된 요청입니다.") val message = e.messageKey?.takeIf { it.isNotBlank() }?.let { messageSource.getMessage(it, langContext.lang) }
?: e.message?.takeIf { it.isNotBlank() }
?: messageSource.getMessage("common.error.invalid_request", langContext.lang)
ApiResponse.error(message)
} }
@ExceptionHandler(Exception::class) @ExceptionHandler(Exception::class)
fun handleException(e: Exception) = run { fun handleException(e: Exception) = run {
if (e is ResponseStatusException) throw e if (e is ResponseStatusException) throw e
logger.error("API error", e) logger.error("API error", e)
ApiResponse.error("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") val message = messageSource.getMessage("common.error.unknown", langContext.lang)
ApiResponse.error(message)
} }
} }

View File

@@ -36,7 +36,7 @@ class AudioContentController(private val service: AudioContentService) {
@RequestPart("request") requestString: String, @RequestPart("request") requestString: 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( ApiResponse.ok(
service.createAudioContent( service.createAudioContent(
@@ -57,7 +57,7 @@ class AudioContentController(private val service: AudioContentService) {
@RequestPart("request") requestString: String, @RequestPart("request") requestString: 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( ApiResponse.ok(
service.modifyAudioContent( service.modifyAudioContent(
@@ -74,7 +74,7 @@ class AudioContentController(private val service: AudioContentService) {
@RequestBody request: UploadCompleteRequest, @RequestBody request: UploadCompleteRequest,
@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( ApiResponse.ok(
service.uploadComplete( service.uploadComplete(
@@ -91,7 +91,7 @@ class AudioContentController(private val service: AudioContentService) {
@PathVariable id: Long, @PathVariable id: Long,
@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( ApiResponse.ok(
service.deleteAudioContent( service.deleteAudioContent(
@@ -111,7 +111,7 @@ class AudioContentController(private val service: AudioContentService) {
@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( ApiResponse.ok(
service.getAudioContentList( service.getAudioContentList(
@@ -134,7 +134,7 @@ class AudioContentController(private val service: AudioContentService) {
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
@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( ApiResponse.ok(
service.getDetail( service.getDetail(
@@ -151,7 +151,7 @@ class AudioContentController(private val service: AudioContentService) {
@PathVariable id: Long, @PathVariable id: Long,
@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.generateUrl(contentId = id, member = member)) ApiResponse.ok(service.generateUrl(contentId = id, member = member))
} }
@@ -160,7 +160,7 @@ class AudioContentController(private val service: AudioContentService) {
@RequestBody request: AddAllPlaybackTrackingRequest, @RequestBody request: AddAllPlaybackTrackingRequest,
@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.addAllPlaybackTracking(request, member)) ApiResponse.ok(service.addAllPlaybackTracking(request, member))
} }
@@ -170,7 +170,7 @@ class AudioContentController(private val service: AudioContentService) {
@RequestBody request: PutAudioContentLikeRequest, @RequestBody request: PutAudioContentLikeRequest,
@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.audioContentLike(request, member)) ApiResponse.ok(service.audioContentLike(request, member))
} }
@@ -179,7 +179,7 @@ class AudioContentController(private val service: AudioContentService) {
fun getAudioContentRankingSort( fun getAudioContentRankingSort(
@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.getContentRankingSortTypeList()) ApiResponse.ok(service.getContentRankingSortTypeList())
} }
@@ -221,7 +221,7 @@ class AudioContentController(private val service: AudioContentService) {
@PathVariable id: Long, @PathVariable id: Long,
@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.pinToTheTop(contentId = id, member = member)) ApiResponse.ok(service.pinToTheTop(contentId = id, member = member))
} }
@@ -232,7 +232,7 @@ class AudioContentController(private val service: AudioContentService) {
@PathVariable id: Long, @PathVariable id: Long,
@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.unpinAtTheTop(contentId = id, member = member)) ApiResponse.ok(service.unpinAtTheTop(contentId = id, member = member))
} }
@@ -248,7 +248,7 @@ class AudioContentController(private val service: AudioContentService) {
@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( ApiResponse.ok(
service.getLatestContentByTheme( service.getLatestContentByTheme(

View File

@@ -31,6 +31,7 @@ import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
@@ -74,6 +75,7 @@ class AudioContentService(
private val audioContentCloudFront: AudioContentCloudFront, private val audioContentCloudFront: AudioContentCloudFront,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val messageSource: SodaMessageSource,
private val langContext: LangContext, private val langContext: LangContext,
private val contentThemeTranslationRepository: ContentThemeTranslationRepository, private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
@@ -117,7 +119,7 @@ class AudioContentService(
val request = objectMapper.readValue(requestString, ModifyAudioContentRequest::class.java) val request = objectMapper.readValue(requestString, ModifyAudioContentRequest::class.java)
val audioContent = repository.findByIdAndCreatorId(request.contentId, member.id!!) val audioContent = repository.findByIdAndCreatorId(request.contentId, member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
if (request.title != null) audioContent.title = request.title if (request.title != null) audioContent.title = request.title
if (request.detail != null) audioContent.detail = request.detail if (request.detail != null) audioContent.detail = request.detail
@@ -189,7 +191,7 @@ class AudioContentService(
@Transactional @Transactional
fun deleteAudioContent(audioContentId: Long, member: Member) { fun deleteAudioContent(audioContentId: Long, member: Member) {
val audioContent = repository.findByIdAndCreatorId(audioContentId, member.id!!) val audioContent = repository.findByIdAndCreatorId(audioContentId, member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
audioContent.isActive = false audioContent.isActive = false
audioContent.releaseDate = null audioContent.releaseDate = null
@@ -203,7 +205,7 @@ class AudioContentService(
member: Member member: Member
): CreateAudioContentResponse { ): CreateAudioContentResponse {
// coverImage 체크 // coverImage 체크
if (coverImage == null) throw SodaException("커버이미지를 선택해 주세요.") if (coverImage == null) throw SodaException(messageKey = "content.error.cover_image_required")
// request 내용 파싱 // request 내용 파싱
val request = objectMapper.readValue(requestString, CreateAudioContentRequest::class.java) val request = objectMapper.readValue(requestString, CreateAudioContentRequest::class.java)
@@ -222,18 +224,18 @@ class AudioContentService(
// contentFile 체크 // contentFile 체크
if (contentFile == null) { if (contentFile == null) {
throw SodaException("콘텐츠를 선택해 주세요.") throw SodaException(messageKey = "content.error.content_required")
} }
// 테마 체크 // 테마 체크
val theme = themeQueryRepository.findThemeByIdAndActive(id = request.themeId) val theme = themeQueryRepository.findThemeByIdAndActive(id = request.themeId)
?: throw SodaException("잘못된 테마입니다. 다시 선택해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_theme")
if ((request.themeId == 12L || request.themeId == 13L || request.themeId == 14L) && request.price < 5) { if ((request.themeId == 12L || request.themeId == 13L || request.themeId == 14L) && request.price < 5) {
throw SodaException("알람, 모닝콜, 슬립콜 테마의 콘텐츠는 5캔 이상의 유료콘텐츠로 등록이 가능합니다.") throw SodaException(messageKey = "content.error.alarm_theme_price_min")
} }
if (request.price in 1..4) throw SodaException("콘텐츠의 최소금액은 5캔 입니다.") if (request.price in 1..4) throw SodaException(messageKey = "content.error.minimum_price")
val isFullDetailVisible = if (request.price >= 50) { val isFullDetailVisible = if (request.price >= 50) {
request.isFullDetailVisible request.isFullDetailVisible
@@ -388,34 +390,34 @@ class AudioContentService(
if (previewStartTime != null && previewEndTime != null) { if (previewStartTime != null && previewEndTime != null) {
val startTimeArray = previewStartTime.split(":") val startTimeArray = previewStartTime.split(":")
if (startTimeArray.size != 3) { if (startTimeArray.size != 3) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다") throw SodaException(messageKey = "content.error.preview_time_format")
} }
for (time in startTimeArray) { for (time in startTimeArray) {
if (time.length != 2) { if (time.length != 2) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다") throw SodaException(messageKey = "content.error.preview_time_format")
} }
} }
val endTimeArray = previewEndTime.split(":") val endTimeArray = previewEndTime.split(":")
if (endTimeArray.size != 3) { if (endTimeArray.size != 3) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다") throw SodaException(messageKey = "content.error.preview_time_format")
} }
for (time in endTimeArray) { for (time in endTimeArray) {
if (time.length != 2) { if (time.length != 2) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다") throw SodaException(messageKey = "content.error.preview_time_format")
} }
} }
val timeDifference = timeDifference(previewStartTime, previewEndTime) val timeDifference = timeDifference(previewStartTime, previewEndTime)
if (timeDifference < 15000) { if (timeDifference < 15000) {
throw SodaException("미리 듣기의 최소 시간은 15초 입니다.") throw SodaException(messageKey = "content.error.preview_time_minimum")
} }
} else { } else {
if (previewStartTime != null || previewEndTime != null) { if (previewStartTime != null || previewEndTime != null) {
throw SodaException("미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다.") throw SodaException(messageKey = "content.error.preview_time_both_required")
} }
} }
} }
@@ -445,10 +447,10 @@ class AudioContentService(
@Transactional @Transactional
fun uploadComplete(contentId: Long, content: String, duration: String) { fun uploadComplete(contentId: Long, content: String, duration: String) {
val keyFileName = content.split("/").last() val keyFileName = content.split("/").last()
if (!keyFileName.startsWith(contentId.toString())) throw SodaException("잘못된 요청입니다.") if (!keyFileName.startsWith(contentId.toString())) throw SodaException(messageKey = "common.error.invalid_request")
val audioContent = repository.findByIdOrNull(contentId) val audioContent = repository.findByIdOrNull(contentId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
audioContent.content = content audioContent.content = content
audioContent.duration = duration audioContent.duration = duration
@@ -456,7 +458,7 @@ class AudioContentService(
applicationEventPublisher.publishEvent( applicationEventPublisher.publishEvent(
FcmEvent( FcmEvent(
type = FcmEventType.INDIVIDUAL, type = FcmEventType.INDIVIDUAL,
title = "콘텐츠 등록완료", title = formatMessage("content.notification.upload_complete_title"),
message = audioContent.title, message = audioContent.title,
recipients = listOf(audioContent.member!!.id!!), recipients = listOf(audioContent.member!!.id!!),
isAuth = null, isAuth = null,
@@ -471,7 +473,7 @@ class AudioContentService(
FcmEvent( FcmEvent(
type = FcmEventType.UPLOAD_CONTENT, type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname, title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult, isAuth = audioContent.isAdult,
contentId = contentId, contentId = contentId,
creatorId = audioContent.member!!.id, creatorId = audioContent.member!!.id,
@@ -483,7 +485,7 @@ class AudioContentService(
FcmEvent( FcmEvent(
type = FcmEventType.UPLOAD_CONTENT, type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname, title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult, isAuth = audioContent.isAdult,
contentId = contentId, contentId = contentId,
creatorId = audioContent.member!!.id, creatorId = audioContent.member!!.id,
@@ -505,7 +507,7 @@ class AudioContentService(
FcmEvent( FcmEvent(
type = FcmEventType.UPLOAD_CONTENT, type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname, title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult, isAuth = audioContent.isAdult,
contentId = audioContent.id!!, contentId = audioContent.id!!,
creatorId = audioContent.member!!.id, creatorId = audioContent.member!!.id,
@@ -517,7 +519,7 @@ class AudioContentService(
FcmEvent( FcmEvent(
type = FcmEventType.UPLOAD_CONTENT, type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname, title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", message = formatMessage("content.notification.uploaded_message", audioContent.title),
isAuth = audioContent.isAdult, isAuth = audioContent.isAdult,
contentId = audioContent.id!!, contentId = audioContent.id!!,
creatorId = audioContent.member!!.id, creatorId = audioContent.member!!.id,
@@ -538,12 +540,12 @@ class AudioContentService(
// 오디오 콘텐츠 조회 (content_id, 제목, 내용, 테마, 태그, 19여부, 이미지, 콘텐츠 PATH) // 오디오 콘텐츠 조회 (content_id, 제목, 내용, 테마, 태그, 19여부, 이미지, 콘텐츠 PATH)
val audioContent = repository.findByIdOrNull(id) val audioContent = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
// 크리에이터(유저) 정보 // 크리에이터(유저) 정보
val creatorId = audioContent.member!!.id!! val creatorId = audioContent.member!!.id!!
val creator = explorerQueryRepository.getMember(creatorId) val creator = explorerQueryRepository.getMember(creatorId)
?: throw SodaException("없는 사용자 입니다.") ?: throw SodaException(messageKey = "content.error.user_not_found")
val creatorFollowing = explorerQueryRepository.getCreatorFollowing( val creatorFollowing = explorerQueryRepository.getCreatorFollowing(
creatorId = creatorId, creatorId = creatorId,
@@ -557,7 +559,9 @@ class AudioContentService(
// 차단된 사용자 체크 // 차단된 사용자 체크
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId) val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
if (isBlocked && !isExistsAudioContent) throw SodaException("${creator.nickname}님의 요청으로 콘텐츠 접근이 제한됩니다.") if (isBlocked && !isExistsAudioContent) {
throw SodaException(formatMessage("content.error.access_restricted_by_creator", creator.nickname))
}
val orderSequence = if (isExistsAudioContent) { val orderSequence = if (isExistsAudioContent) {
limitedEditionOrderRepository.getOrderSequence( limitedEditionOrderRepository.getOrderSequence(
@@ -595,7 +599,7 @@ class AudioContentService(
audioContent.releaseDate != null && audioContent.releaseDate != null &&
audioContent.releaseDate!! < LocalDateTime.now() audioContent.releaseDate!! < LocalDateTime.now()
) { ) {
throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") throw SodaException(messageKey = "content.error.invalid_content_retry")
} }
// 댓글 // 댓글
@@ -628,11 +632,13 @@ class AudioContentService(
audioContent.releaseDate != null && audioContent.releaseDate != null &&
audioContent.releaseDate!! >= LocalDateTime.now() audioContent.releaseDate!! >= LocalDateTime.now()
) { ) {
val releaseDatePattern = messageSource.getMessage("content.release_date.format", langContext.lang)
?: "yyyy년 MM월 dd일 HH시 mm분 오픈예정"
audioContent.releaseDate!! audioContent.releaseDate!!
.atZone(ZoneId.of("UTC")) .atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul")) .withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.toLocalDateTime() .toLocalDateTime()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 오픈예정")) .format(DateTimeFormatter.ofPattern(releaseDatePattern, langContext.lang.locale))
} else { } else {
null null
} }
@@ -1114,8 +1120,13 @@ class AudioContentService(
limit: Long, limit: Long,
sortType: String = "매출" sortType: String = "매출"
): GetAudioContentRanking { ): GetAudioContentRanking {
val startDateFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") val normalizedSortType = normalizeRankingSortType(sortType)
val endDateFormatter = DateTimeFormatter.ofPattern("MM월 dd일") val startDatePattern = messageSource.getMessage("content.ranking.date.start_format", langContext.lang)
?: "yyyy년 MM월 dd일"
val endDatePattern = messageSource.getMessage("content.ranking.date.end_format", langContext.lang)
?: "MM월 dd일"
val startDateFormatter = DateTimeFormatter.ofPattern(startDatePattern, langContext.lang.locale)
val endDateFormatter = DateTimeFormatter.ofPattern(endDatePattern, langContext.lang.locale)
val contentRankingItemList = repository val contentRankingItemList = repository
.getAudioContentRanking( .getAudioContentRanking(
@@ -1126,7 +1137,7 @@ class AudioContentService(
contentType = contentType, contentType = contentType,
offset = offset, offset = offset,
limit = limit, limit = limit,
sortType = sortType sortType = normalizedSortType
) )
return GetAudioContentRanking( return GetAudioContentRanking(
@@ -1137,16 +1148,19 @@ class AudioContentService(
} }
fun getContentRankingSortTypeList(): List<String> { fun getContentRankingSortTypeList(): List<String> {
return listOf("매출", "댓글", "좋아요") val salesLabel = messageSource.getMessage("content.ranking.sort_type.sales", langContext.lang) ?: "매출"
val commentLabel = messageSource.getMessage("content.ranking.sort_type.comment", langContext.lang) ?: "댓글"
val likeLabel = messageSource.getMessage("content.ranking.sort_type.like", langContext.lang) ?: "좋아요"
return listOf(salesLabel, commentLabel, likeLabel)
} }
@Transactional @Transactional
fun pinToTheTop(contentId: Long, member: Member) { fun pinToTheTop(contentId: Long, member: Member) {
val audioContent = repository.findByIdAndCreatorId(contentId = contentId, creatorId = member.id!!) val audioContent = repository.findByIdAndCreatorId(contentId = contentId, creatorId = member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
if (audioContent.releaseDate != null && audioContent.releaseDate!! >= LocalDateTime.now()) { if (audioContent.releaseDate != null && audioContent.releaseDate!! >= LocalDateTime.now()) {
throw SodaException("콘텐츠 오픈 후 채널에 고정이 가능합니다.") throw SodaException(messageKey = "content.error.pin_available_after_open")
} }
var pinContent = pinContentRepository.findByContentIdAndMemberId( var pinContent = pinContentRepository.findByContentIdAndMemberId(
@@ -1176,14 +1190,14 @@ class AudioContentService(
val pinContent = pinContentRepository.findByContentIdAndMemberId( val pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId, contentId = contentId,
memberId = member.id!! memberId = member.id!!
) ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ) ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
pinContent.isActive = false pinContent.isActive = false
} }
fun generateUrl(contentId: Long, member: Member): GenerateUrlResponse { fun generateUrl(contentId: Long, member: Member): GenerateUrlResponse {
val audioContent = repository.findByIdOrNull(contentId) val audioContent = repository.findByIdOrNull(contentId)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
val isExistsAudioContent = orderRepository.isExistOrdered( val isExistsAudioContent = orderRepository.isExistOrdered(
memberId = member.id!!, memberId = member.id!!,
@@ -1312,4 +1326,28 @@ class AudioContentService(
.distinct() .distinct()
.toList() .toList()
} }
private fun normalizeRankingSortType(sortType: String?): String {
val trimmed = sortType?.trim().orEmpty()
val internalTypes = setOf("매출", "댓글", "좋아요", "후원")
if (trimmed in internalTypes) return trimmed
val salesLabel = messageSource.getMessage("content.ranking.sort_type.sales", langContext.lang)
val commentLabel = messageSource.getMessage("content.ranking.sort_type.comment", langContext.lang)
val likeLabel = messageSource.getMessage("content.ranking.sort_type.like", langContext.lang)
val donationLabel = messageSource.getMessage("content.ranking.sort_type.donation", langContext.lang)
return when (trimmed) {
salesLabel -> "매출"
commentLabel -> "댓글"
likeLabel -> "좋아요"
donationLabel -> "후원"
else -> "매출"
}
}
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
return String.format(template, *args)
}
} }

View File

@@ -24,7 +24,7 @@ class CategoryController(private val service: CategoryService) {
@RequestBody request: CreateCategoryRequest, @RequestBody request: CreateCategoryRequest,
@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.createCategory(request = request, member = member)) ApiResponse.ok(service.createCategory(request = request, member = member))
} }
@@ -35,7 +35,7 @@ class CategoryController(private val service: CategoryService) {
@RequestBody request: ModifyCategoryRequest, @RequestBody request: ModifyCategoryRequest,
@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.modifyCategory(request = request, member = member)) ApiResponse.ok(service.modifyCategory(request = request, member = member))
} }
@@ -46,7 +46,7 @@ class CategoryController(private val service: CategoryService) {
@RequestBody request: UpdateCategoryOrdersRequest, @RequestBody request: UpdateCategoryOrdersRequest,
@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.updateCategoryOrders(request = request, member = member)) ApiResponse.ok(service.updateCategoryOrders(request = request, member = member))
} }
@@ -57,7 +57,7 @@ class CategoryController(private val service: CategoryService) {
@PathVariable("id") categoryId: Long, @PathVariable("id") categoryId: Long,
@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.deleteCategory(categoryId = categoryId, member = member)) ApiResponse.ok(service.deleteCategory(categoryId = categoryId, member = member))
} }
@@ -67,7 +67,7 @@ class CategoryController(private val service: CategoryService) {
@RequestParam creatorId: Long, @RequestParam creatorId: Long,
@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.getCategoryList(creatorId = creatorId, memberId = member.id!!)) ApiResponse.ok(service.getCategoryList(creatorId = creatorId, memberId = member.id!!))
} }

View File

@@ -66,7 +66,7 @@ class CategoryService(
@Transactional @Transactional
fun modifyCategory(request: ModifyCategoryRequest, member: Member) { fun modifyCategory(request: ModifyCategoryRequest, member: Member) {
val category = repository.findByIdAndMemberId(categoryId = request.categoryId, memberId = member.id!!) val category = repository.findByIdAndMemberId(categoryId = request.categoryId, memberId = member.id!!)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (!request.title.isNullOrBlank()) { if (!request.title.isNullOrBlank()) {
validateTitle(title = request.title) validateTitle(title = request.title)
@@ -108,7 +108,7 @@ class CategoryService(
@Transactional @Transactional
fun deleteCategory(categoryId: Long, member: Member) { fun deleteCategory(categoryId: Long, member: Member) {
val category = repository.findByIdAndMemberId(categoryId = categoryId, memberId = member.id!!) val category = repository.findByIdAndMemberId(categoryId = categoryId, memberId = member.id!!)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
category.isActive = false category.isActive = false
categoryContentRepository.deleteByCategoryId(categoryId = categoryId) categoryContentRepository.deleteByCategoryId(categoryId = categoryId)
@@ -128,7 +128,7 @@ class CategoryService(
@Transactional @Transactional
fun getCategoryList(creatorId: Long, memberId: Long): List<GetCategoryListResponse> { fun getCategoryList(creatorId: Long, memberId: Long): List<GetCategoryListResponse> {
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId)
if (isBlocked) throw SodaException("잘못된 접근입니다.") if (isBlocked) throw SodaException(messageKey = "category.error.invalid_access")
// 기본 카테고리 목록 조회 (원본 언어 기준) // 기본 카테고리 목록 조회 (원본 언어 기준)
val baseList = repository.findByCreatorId(creatorId = creatorId) val baseList = repository.findByCreatorId(creatorId = creatorId)
@@ -205,6 +205,6 @@ class CategoryService(
} }
private fun validateTitle(title: String) { private fun validateTitle(title: String) {
if (title.length < 2) throw SodaException("카테고리명은 2글자 이상 입력하세요") if (title.length < 2) throw SodaException(messageKey = "category.error.title_min_length")
} }
} }

View File

@@ -25,7 +25,7 @@ class AudioContentCommentController(
@RequestBody request: RegisterCommentRequest, @RequestBody request: RegisterCommentRequest,
@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")
val commentId = service.registerComment( val commentId = service.registerComment(
comment = request.comment, comment = request.comment,
@@ -62,7 +62,7 @@ class AudioContentCommentController(
@RequestBody request: ModifyCommentRequest, @RequestBody request: ModifyCommentRequest,
@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.modifyComment(request = request, member = member)) ApiResponse.ok(service.modifyComment(request = request, member = member))
} }
@@ -74,7 +74,7 @@ class AudioContentCommentController(
@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( ApiResponse.ok(
service.getCommentList( service.getCommentList(
@@ -93,7 +93,7 @@ class AudioContentCommentController(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable pageable: Pageable
): ApiResponse<GetAudioContentCommentListResponse> { ): ApiResponse<GetAudioContentCommentListResponse> {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
return ApiResponse.ok( return ApiResponse.ok(
service.getCommentReplyList( service.getCommentReplyList(

View File

@@ -7,6 +7,8 @@ import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
import kr.co.vividnext.sodalive.content.order.OrderRepository import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType 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.Member
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
@@ -24,6 +26,8 @@ class AudioContentCommentService(
private val audioContentRepository: AudioContentRepository, private val audioContentRepository: AudioContentRepository,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val orderRepository: OrderRepository, private val orderRepository: OrderRepository,
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
@@ -38,11 +42,13 @@ class AudioContentCommentService(
languageCode: String? languageCode: String?
): Long { ): Long {
val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId) val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
val creator = audioContent.member!! val creator = audioContent.member!!
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creator.id!!) val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creator.id!!)
if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 댓글쓰기가 제한됩니다.") if (isBlocked) {
throw SodaException(formatMessage("content.comment.error.blocked_by_creator", creator.nickname))
}
val (isExistsAudioContent, _) = orderRepository.isExistOrderedAndOrderType( val (isExistsAudioContent, _) = orderRepository.isExistOrderedAndOrderType(
memberId = member.id!!, memberId = member.id!!,
@@ -50,7 +56,7 @@ class AudioContentCommentService(
) )
if (isSecret && !isExistsAudioContent) { if (isSecret && !isExistsAudioContent) {
throw SodaException("콘텐츠 구매 후 비밀댓글을 등록할 수 있습니다.") throw SodaException(messageKey = "content.comment.error.secret_requires_purchase")
} }
val audioContentComment = AudioContentComment(comment = comment, languageCode = languageCode, isSecret = isSecret) val audioContentComment = AudioContentComment(comment = comment, languageCode = languageCode, isSecret = isSecret)
@@ -78,9 +84,9 @@ class AudioContentCommentService(
member.nickname member.nickname
}, },
message = if (parent != null) { message = if (parent != null) {
"댓글에 답글을 달았습니다.: ${audioContent.title}" formatMessage("content.comment.notification.reply", audioContent.title)
} else { } else {
"콘텐츠에 댓글을 달았습니다.: ${audioContent.title}" formatMessage("content.comment.notification.new", audioContent.title)
}, },
contentId = audioContentId, contentId = audioContentId,
commentParentId = parentId, commentParentId = parentId,
@@ -105,7 +111,7 @@ class AudioContentCommentService(
@Transactional @Transactional
fun modifyComment(request: ModifyCommentRequest, member: Member) { fun modifyComment(request: ModifyCommentRequest, member: Member) {
val audioContentComment = repository.findByIdOrNull(request.commentId) val audioContentComment = repository.findByIdOrNull(request.commentId)
?: throw SodaException("잘못된 접근 입니다.\n확인 후 다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.comment.error.invalid_access_retry")
if (audioContentComment.member!!.id!! == member.id!!) { if (audioContentComment.member!!.id!! == member.id!!) {
if (request.comment != null) { if (request.comment != null) {
@@ -164,4 +170,9 @@ class AudioContentCommentService(
return GetAudioContentCommentListResponse(totalCount, commentList) return GetAudioContentCommentListResponse(totalCount, commentList)
} }
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
return String.format(template, *args)
}
} }

View File

@@ -18,7 +18,7 @@ class AudioContentDonationController(private val service: AudioContentDonationSe
@RequestBody request: AudioContentDonationRequest, @RequestBody request: AudioContentDonationRequest,
@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.donation(request = request, member = member)) ApiResponse.ok(service.donation(request = request, member = member))
} }

View File

@@ -22,11 +22,11 @@ class AudioContentDonationService(
) { ) {
@Transactional @Transactional
fun donation(request: AudioContentDonationRequest, member: Member) { fun donation(request: AudioContentDonationRequest, member: Member) {
if (request.donationCan < 1) throw SodaException("1캔 이상 후원하실 수 있습니다.") if (request.donationCan < 1) throw SodaException(messageKey = "content.donation.error.minimum_can")
if (request.comment.isBlank()) throw SodaException("함께 보낼 메시지를 입력하세요.") if (request.comment.isBlank()) throw SodaException(messageKey = "content.donation.error.comment_required")
val audioContent = queryRepository.findByIdAndActive(request.contentId) val audioContent = queryRepository.findByIdAndActive(request.contentId)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
canPaymentService.spendCan( canPaymentService.spendCan(
memberId = member.id!!, memberId = member.id!!,

View File

@@ -22,7 +22,7 @@ class AudioContentMainController(
fun newContentUploadCreatorList( fun newContentUploadCreatorList(
@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( ApiResponse.ok(
service.getNewContentUploadCreatorList( service.getNewContentUploadCreatorList(
@@ -36,7 +36,7 @@ class AudioContentMainController(
fun getMainBannerList( fun getMainBannerList(
@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( ApiResponse.ok(
service.getAudioContentMainBannerList( service.getAudioContentMainBannerList(
@@ -50,7 +50,7 @@ class AudioContentMainController(
fun getMainOrderList( fun getMainOrderList(
@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( ApiResponse.ok(
orderService.getAudioContentMainOrderList( orderService.getAudioContentMainOrderList(
@@ -68,7 +68,7 @@ class AudioContentMainController(
@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( ApiResponse.ok(
service.getNewContentByTheme( service.getNewContentByTheme(
@@ -87,7 +87,7 @@ class AudioContentMainController(
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@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( ApiResponse.ok(
service.getThemeList( service.getThemeList(
@@ -105,7 +105,7 @@ class AudioContentMainController(
@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( ApiResponse.ok(
service.getNewContentFor2WeeksByTheme( service.getNewContentFor2WeeksByTheme(
@@ -125,7 +125,7 @@ class AudioContentMainController(
@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( ApiResponse.ok(
service.getAudioContentCurationListWithPaging( service.getAudioContentCurationListWithPaging(

View File

@@ -19,7 +19,7 @@ class OrderController(private val service: OrderService) {
@RequestBody request: OrderRequest, @RequestBody request: OrderRequest,
@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( ApiResponse.ok(
service.order( service.order(
@@ -36,7 +36,7 @@ class OrderController(private val service: OrderService) {
@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( ApiResponse.ok(
service.getAudioContentOrderList( service.getAudioContentOrderList(

View File

@@ -33,11 +33,11 @@ class OrderService(
@Transactional @Transactional
fun order(contentId: Long, orderType: OrderType, container: String, member: Member) { fun order(contentId: Long, orderType: OrderType, container: String, member: Member) {
val content = audioContentRepository.findByIdAndActive(contentId) val content = audioContentRepository.findByIdAndActive(contentId)
?: throw SodaException("잘못된 콘텐츠 입니다\n다시 시도해 주세요.") ?: throw SodaException(messageKey = "content.error.invalid_content_retry")
validateOrder(memberId = member.id!!, content = content, orderType = orderType) validateOrder(memberId = member.id!!, content = content, orderType = orderType)
val order = if (content.limited != null && content.remaining != null) { val order = if (content.limited != null && content.remaining != null) {
if (content.remaining!! <= 0) throw SodaException("해당 콘텐츠가 매진되었습니다.") if (content.remaining!! <= 0) throw SodaException(messageKey = "order.error.content_sold_out")
orderLimitedEditionContent(content, member) orderLimitedEditionContent(content, member)
} else { } else {
orderContent(orderType, content, member) orderContent(orderType, content, member)
@@ -93,16 +93,20 @@ class OrderService(
} }
private fun validateOrder(memberId: Long, content: AudioContent, orderType: OrderType) { private fun validateOrder(memberId: Long, content: AudioContent, orderType: OrderType) {
if (memberId == content.member!!.id!!) throw SodaException("자신이 올린 콘텐츠는 구매할 수 없습니다.") if (memberId == content.member!!.id!!) {
throw SodaException(messageKey = "order.error.cannot_purchase_own_content")
}
val existOrdered = repository.isExistOrdered(memberId = memberId, contentId = content.id!!) val existOrdered = repository.isExistOrdered(memberId = memberId, contentId = content.id!!)
if (existOrdered) throw SodaException("이미 구매한 콘텐츠 입니다.") if (existOrdered) throw SodaException(messageKey = "order.error.already_purchased")
val isOnlyRental = content.purchaseOption == PurchaseOption.RENT_ONLY || content.isOnlyRental val isOnlyRental = content.purchaseOption == PurchaseOption.RENT_ONLY || content.isOnlyRental
if (isOnlyRental && orderType == OrderType.KEEP) throw SodaException("대여만 가능한 콘텐츠 입니다.") if (isOnlyRental && orderType == OrderType.KEEP) {
throw SodaException(messageKey = "order.error.rental_only")
}
val isOnlyBuy = content.purchaseOption == PurchaseOption.BUY_ONLY && orderType == OrderType.RENTAL val isOnlyBuy = content.purchaseOption == PurchaseOption.BUY_ONLY && orderType == OrderType.RENTAL
if (isOnlyBuy) throw SodaException("소장만 가능한 콘텐츠 입니다.\n앱 업데이트 후 구매해 주세요.") if (isOnlyBuy) throw SodaException(messageKey = "order.error.keep_only_update_required")
} }
fun getAudioContentOrderList( fun getAudioContentOrderList(

View File

@@ -21,7 +21,7 @@ class AudioContentPlaylistController(private val service: AudioContentPlaylistSe
@RequestBody request: CreatePlaylistRequest, @RequestBody request: CreatePlaylistRequest,
@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.createPlaylist(request, member)) ApiResponse.ok(service.createPlaylist(request, member))
} }
@@ -32,7 +32,7 @@ class AudioContentPlaylistController(private val service: AudioContentPlaylistSe
@RequestBody request: UpdatePlaylistRequest, @RequestBody request: UpdatePlaylistRequest,
@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.updatePlaylist(playlistId = id, request = request, member = member)) ApiResponse.ok(service.updatePlaylist(playlistId = id, request = request, member = member))
} }
@@ -42,7 +42,7 @@ class AudioContentPlaylistController(private val service: AudioContentPlaylistSe
@PathVariable id: Long, @PathVariable id: Long,
@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.deletePlaylist(playlistId = id, member)) ApiResponse.ok(service.deletePlaylist(playlistId = id, member))
} }
@@ -51,7 +51,7 @@ class AudioContentPlaylistController(private val service: AudioContentPlaylistSe
fun getPlaylists( fun getPlaylists(
@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.getPlaylists(member)) ApiResponse.ok(service.getPlaylists(member))
} }
@@ -61,7 +61,7 @@ class AudioContentPlaylistController(private val service: AudioContentPlaylistSe
@PathVariable id: Long, @PathVariable id: Long,
@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.getPlaylistDetail(playlistId = id, member = member)) ApiResponse.ok(service.getPlaylistDetail(playlistId = id, member = member))
} }

View File

@@ -20,12 +20,12 @@ class AudioContentPlaylistService(
) { ) {
fun createPlaylist(request: CreatePlaylistRequest, member: Member) { fun createPlaylist(request: CreatePlaylistRequest, member: Member) {
if (request.contentIdAndOrderList.size >= 30) { if (request.contentIdAndOrderList.size >= 30) {
throw SodaException("플레이 리스트에는 최대 30개의 콘텐츠를 저장할 수 있습니다.") throw SodaException(messageKey = "playlist.error.max_content_limit")
} }
val playlistCount = redisRepository.findByMemberId(member.id!!).size val playlistCount = redisRepository.findByMemberId(member.id!!).size
if (playlistCount >= 10) { if (playlistCount >= 10) {
throw SodaException("플레이 리스트는 최대 10개까지 생성할 수 있습니다.") throw SodaException(messageKey = "playlist.error.max_playlist_limit")
} }
val contentIdAndOrderList = validateAndGetContentIdAndOrderList( val contentIdAndOrderList = validateAndGetContentIdAndOrderList(
@@ -68,7 +68,7 @@ class AudioContentPlaylistService(
private fun validateContent(contentIdList: List<Long>, memberId: Long) { private fun validateContent(contentIdList: List<Long>, memberId: Long) {
if (contentIdList.isEmpty()) { if (contentIdList.isEmpty()) {
throw SodaException("콘텐츠를 1개 이상 추가하세요") throw SodaException(messageKey = "playlist.error.content_required")
} }
checkOrderedContent( checkOrderedContent(
@@ -83,20 +83,20 @@ class AudioContentPlaylistService(
val notOrderedContentList = orderedContentMap.filterValues { !it }.keys val notOrderedContentList = orderedContentMap.filterValues { !it }.keys
if (notOrderedContentList.isNotEmpty()) { if (notOrderedContentList.isNotEmpty()) {
throw SodaException("대여/소장하지 않은 콘텐츠는 재생목록에 추가할 수 없습니다.") throw SodaException(messageKey = "playlist.error.not_purchased_content")
} }
} }
fun updatePlaylist(playlistId: Long, request: UpdatePlaylistRequest, member: Member) { fun updatePlaylist(playlistId: Long, request: UpdatePlaylistRequest, member: Member) {
if (request.contentIdAndOrderList.size >= 30) { if (request.contentIdAndOrderList.size >= 30) {
throw SodaException("플레이 리스트에는 최대 30개의 콘텐츠를 저장할 수 있습니다.") throw SodaException(messageKey = "playlist.error.max_content_limit")
} }
val playlist = redisRepository.findByIdOrNull(id = playlistId) val playlist = redisRepository.findByIdOrNull(id = playlistId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (playlist.memberId != member.id) { if (playlist.memberId != member.id) {
throw SodaException("잘못된 요청입니다.") throw SodaException(messageKey = "common.error.invalid_request")
} }
val contentIdAndOrderList = validateAndGetContentIdAndOrderList( val contentIdAndOrderList = validateAndGetContentIdAndOrderList(
@@ -145,10 +145,10 @@ class AudioContentPlaylistService(
fun deletePlaylist(playlistId: Long, member: Member) { fun deletePlaylist(playlistId: Long, member: Member) {
val playlist = redisRepository.findByIdOrNull(id = playlistId) val playlist = redisRepository.findByIdOrNull(id = playlistId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (playlist.memberId != member.id) { if (playlist.memberId != member.id) {
throw SodaException("잘못된 요청입니다.") throw SodaException(messageKey = "common.error.invalid_request")
} }
redisRepository.delete(playlist) redisRepository.delete(playlist)
@@ -156,10 +156,10 @@ class AudioContentPlaylistService(
fun getPlaylistDetail(playlistId: Long, member: Member): GetPlaylistDetailResponse { fun getPlaylistDetail(playlistId: Long, member: Member): GetPlaylistDetailResponse {
val playlist = redisRepository.findByIdOrNull(id = playlistId) val playlist = redisRepository.findByIdOrNull(id = playlistId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
if (playlist.memberId != member.id) { if (playlist.memberId != member.id) {
throw SodaException("잘못된 요청입니다.") throw SodaException(messageKey = "common.error.invalid_request")
} }
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd")

View File

@@ -26,7 +26,7 @@ class ContentSeriesController(private val service: ContentSeriesService) {
@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( ApiResponse.ok(
service.getSeriesList( service.getSeriesList(
@@ -49,7 +49,7 @@ class ContentSeriesController(private val service: ContentSeriesService) {
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@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( ApiResponse.ok(
service.getSeriesDetail( service.getSeriesDetail(
@@ -70,7 +70,7 @@ class ContentSeriesController(private val service: ContentSeriesService) {
@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( ApiResponse.ok(
service.getSeriesContentList( service.getSeriesContentList(
@@ -91,7 +91,7 @@ class ContentSeriesController(private val service: ContentSeriesService) {
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@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( ApiResponse.ok(
service.getRecommendSeriesList( service.getRecommendSeriesList(

View File

@@ -210,11 +210,11 @@ class ContentSeriesService(
seriesId = seriesId, seriesId = seriesId,
isAuth = member.auth != null && isAdultContentVisible, isAuth = member.auth != null && isAdultContentVisible,
contentType = contentType contentType = contentType
) ?: throw SodaException("잘못된 시리즈 입니다.\n다시 시도해 주세요") ) ?: throw SodaException(messageKey = "series.error.invalid_series_retry")
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = series.member!!.id!!) val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = series.member!!.id!!)
if (isBlocked) { if (isBlocked) {
throw SodaException("잘못된 시리즈 입니다.\n다시 시도해 주세요") throw SodaException(messageKey = "series.error.invalid_series_retry")
} }
val creatorFollowing = explorerQueryRepository.getCreatorFollowing( val creatorFollowing = explorerQueryRepository.getCreatorFollowing(

View File

@@ -31,7 +31,7 @@ class SeriesMainController(
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@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")
val banners = bannerService.getActiveBanners(PageRequest.of(0, 10)) val banners = bannerService.getActiveBanners(PageRequest.of(0, 10))
.content .content
@@ -70,7 +70,7 @@ class SeriesMainController(
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@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( ApiResponse.ok(
contentSeriesService.getRecommendSeriesList( contentSeriesService.getRecommendSeriesList(
@@ -90,7 +90,7 @@ class SeriesMainController(
@RequestParam(defaultValue = "20") size: Int, @RequestParam(defaultValue = "20") size: Int,
@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")
val pageable = PageRequest.of(page, size) val pageable = PageRequest.of(page, size)
ApiResponse.ok( ApiResponse.ok(
@@ -111,7 +111,7 @@ class SeriesMainController(
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@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")
val memberId = member.id!! val memberId = member.id!!
val isAdult = member.auth != null && (isAdultContentVisible ?: true) val isAdult = member.auth != null && (isAdultContentVisible ?: true)
@@ -134,7 +134,7 @@ class SeriesMainController(
@RequestParam(defaultValue = "20") size: Int, @RequestParam(defaultValue = "20") size: Int,
@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")
val pageable = PageRequest.of(page, size) val pageable = PageRequest.of(page, size)
ApiResponse.ok( ApiResponse.ok(

View File

@@ -18,13 +18,13 @@ class ContentSeriesBannerService(
fun getBannerById(bannerId: Long): SeriesBanner { fun getBannerById(bannerId: Long): SeriesBanner {
return bannerRepository.findById(bannerId) return bannerRepository.findById(bannerId)
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } .orElseThrow { SodaException(messageKey = "series.banner.error.not_found") }
} }
@Transactional @Transactional
fun registerBanner(seriesId: Long, imagePath: String): SeriesBanner { fun registerBanner(seriesId: Long, imagePath: String): SeriesBanner {
val series = seriesRepository.findByIdAndActiveTrue(seriesId) val series = seriesRepository.findByIdAndActiveTrue(seriesId)
?: throw SodaException("시리즈를 찾을 수 없습니다: $seriesId") ?: throw SodaException(messageKey = "series.banner.error.series_not_found")
val finalSortOrder = (bannerRepository.findMaxSortOrder() ?: 0) + 1 val finalSortOrder = (bannerRepository.findMaxSortOrder() ?: 0) + 1
@@ -43,14 +43,14 @@ class ContentSeriesBannerService(
seriesId: Long? = null seriesId: Long? = null
): SeriesBanner { ): SeriesBanner {
val banner = bannerRepository.findById(bannerId) val banner = bannerRepository.findById(bannerId)
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } .orElseThrow { SodaException(messageKey = "series.banner.error.not_found") }
if (!banner.isActive) throw SodaException("비활성화된 배너는 수정할 수 없습니다: $bannerId") if (!banner.isActive) throw SodaException(messageKey = "series.banner.error.inactive_cannot_update")
if (imagePath != null) banner.imagePath = imagePath if (imagePath != null) banner.imagePath = imagePath
if (seriesId != null) { if (seriesId != null) {
val series = seriesRepository.findByIdAndActiveTrue(seriesId) val series = seriesRepository.findByIdAndActiveTrue(seriesId)
?: throw SodaException("시리즈를 찾을 수 없습니다: $seriesId") ?: throw SodaException(messageKey = "series.banner.error.series_not_found")
banner.series = series banner.series = series
} }
@@ -60,7 +60,7 @@ class ContentSeriesBannerService(
@Transactional @Transactional
fun deleteBanner(bannerId: Long) { fun deleteBanner(bannerId: Long) {
val banner = bannerRepository.findById(bannerId) val banner = bannerRepository.findById(bannerId)
.orElseThrow { SodaException("배너를 찾을 수 없습니다: $bannerId") } .orElseThrow { SodaException(messageKey = "series.banner.error.not_found") }
banner.isActive = false banner.isActive = false
bannerRepository.save(banner) bannerRepository.save(banner)
} }
@@ -70,8 +70,8 @@ class ContentSeriesBannerService(
val updated = mutableListOf<SeriesBanner>() val updated = mutableListOf<SeriesBanner>()
for (index in ids.indices) { for (index in ids.indices) {
val banner = bannerRepository.findById(ids[index]) val banner = bannerRepository.findById(ids[index])
.orElseThrow { SodaException("배너를 찾을 수 없습니다: ${ids[index]}") } .orElseThrow { SodaException(messageKey = "series.banner.error.not_found") }
if (!banner.isActive) throw SodaException("비활성화된 배너는 수정할 수 없습니다: ${ids[index]}") if (!banner.isActive) throw SodaException(messageKey = "series.banner.error.inactive_cannot_update")
banner.sortOrder = index + 1 banner.sortOrder = index + 1
updated.add(bannerRepository.save(banner)) updated.add(bannerRepository.save(banner))
} }

View File

@@ -22,7 +22,7 @@ class AudioContentThemeController(private val service: AudioContentThemeService)
fun getThemes( fun getThemes(
@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.getThemes()) ApiResponse.ok(service.getThemes())
} }
@@ -35,7 +35,7 @@ class AudioContentThemeController(private val service: AudioContentThemeService)
@RequestParam("contentType", required = false) contentType: ContentType? = null, @RequestParam("contentType", required = false) contentType: ContentType? = null,
@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( ApiResponse.ok(
service.getActiveThemeOfContent( service.getActiveThemeOfContent(
@@ -56,7 +56,7 @@ class AudioContentThemeController(private val service: AudioContentThemeService)
@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( ApiResponse.ok(
service.getContentByTheme( service.getContentByTheme(

View File

@@ -119,7 +119,7 @@ class AudioContentThemeService(
limit: Long limit: Long
): GetContentByThemeResponse { ): GetContentByThemeResponse {
val theme = queryRepository.findThemeByIdAndActive(themeId) val theme = queryRepository.findThemeByIdAndActive(themeId)
?: throw SodaException("잘못된 요청입니다.") ?: throw SodaException(messageKey = "common.error.invalid_request")
val totalCount = contentRepository.totalCountByTheme( val totalCount = contentRepository.totalCountByTheme(
memberId = member.id!!, memberId = member.id!!,

View File

@@ -21,7 +21,7 @@ class CreatorAdminCalculateController(private val service: CreatorAdminCalculate
@RequestParam endDateStr: String, @RequestParam endDateStr: 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.getCalculateLive(startDateStr, endDateStr, member)) ApiResponse.ok(service.getCalculateLive(startDateStr, endDateStr, member))
} }
@@ -32,7 +32,7 @@ class CreatorAdminCalculateController(private val service: CreatorAdminCalculate
@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( ApiResponse.ok(
service.getCalculateContentList( service.getCalculateContentList(
startDateStr, startDateStr,
@@ -49,7 +49,7 @@ class CreatorAdminCalculateController(private val service: CreatorAdminCalculate
@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.getCumulativeSalesByContent(member.id!!, pageable.offset, pageable.pageSize.toLong())) ApiResponse.ok(service.getCumulativeSalesByContent(member.id!!, pageable.offset, pageable.pageSize.toLong()))
} }
@@ -61,7 +61,7 @@ class CreatorAdminCalculateController(private val service: CreatorAdminCalculate
@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( ApiResponse.ok(
service.getCalculateContentDonationList( service.getCalculateContentDonationList(
startDateStr, startDateStr,
@@ -80,7 +80,7 @@ class CreatorAdminCalculateController(private val service: CreatorAdminCalculate
@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( ApiResponse.ok(
service.getCalculateCommunityPost( service.getCalculateCommunityPost(

View File

@@ -23,7 +23,7 @@ class CreatorAdminContentController(private val service: CreatorAdminContentServ
pageable: Pageable, pageable: Pageable,
@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.getAudioContentList(pageable, member)) ApiResponse.ok(service.getAudioContentList(pageable, member))
} }
@@ -34,7 +34,7 @@ class CreatorAdminContentController(private val service: CreatorAdminContentServ
@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.searchAudioContent(searchWord, member, pageable)) ApiResponse.ok(service.searchAudioContent(searchWord, member, pageable))
} }
@@ -45,7 +45,7 @@ class CreatorAdminContentController(private val service: CreatorAdminContentServ
@RequestPart("request") requestString: String, @RequestPart("request") requestString: 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.updateAudioContent(coverImage, requestString, member)) ApiResponse.ok(service.updateAudioContent(coverImage, requestString, member))
} }

View File

@@ -72,7 +72,7 @@ class CreatorAdminContentService(
} }
fun searchAudioContent(searchWord: String, member: Member, pageable: Pageable): GetCreatorAdminContentListResponse { fun searchAudioContent(searchWord: String, member: Member, pageable: Pageable): GetCreatorAdminContentListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") if (searchWord.length < 2) throw SodaException(messageKey = "creator.admin.content.search_word_min_length")
val totalCount = repository.getAudioContentTotalCount( val totalCount = repository.getAudioContentTotalCount(
memberId = member.id!!, memberId = member.id!!,
searchWord searchWord
@@ -113,7 +113,7 @@ class CreatorAdminContentService(
fun updateAudioContent(coverImage: MultipartFile?, requestString: String, member: Member) { fun updateAudioContent(coverImage: MultipartFile?, requestString: String, member: Member) {
val request = objectMapper.readValue(requestString, UpdateCreatorAdminContentRequest::class.java) val request = objectMapper.readValue(requestString, UpdateCreatorAdminContentRequest::class.java)
val audioContent = repository.getAudioContent(memberId = member.id!!, audioContentId = request.id) val audioContent = repository.getAudioContent(memberId = member.id!!, audioContentId = request.id)
?: throw SodaException("잘못된 콘텐츠 입니다.") ?: throw SodaException(messageKey = "creator.admin.content.invalid_content")
if (coverImage != null) { if (coverImage != null) {
val metadata = ObjectMetadata() val metadata = ObjectMetadata()
@@ -157,7 +157,7 @@ class CreatorAdminContentService(
} }
if (request.price != null) { if (request.price != null) {
if (request.price < 5) throw SodaException("콘텐츠의 최소금액은 5캔 입니다.") if (request.price < 5) throw SodaException(messageKey = "creator.admin.content.min_price")
val contentPriceChangeLog = ContentPriceChangeLog(prevPrice = audioContent.price) val contentPriceChangeLog = ContentPriceChangeLog(prevPrice = audioContent.price)
contentPriceChangeLog.audioContent = audioContent contentPriceChangeLog.audioContent = audioContent

View File

@@ -21,7 +21,7 @@ class CreatorAdminCategoryController(private val service: CreatorAdminCategorySe
@RequestParam(value = "search_word") searchWord: String, @RequestParam(value = "search_word") searchWord: 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( ApiResponse.ok(
service.searchContentNotInCategory( service.searchContentNotInCategory(
@@ -38,7 +38,7 @@ class CreatorAdminCategoryController(private val service: CreatorAdminCategorySe
@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( ApiResponse.ok(
service.getContentInCategory( service.getContentInCategory(

View File

@@ -26,13 +26,13 @@ data class CreateSeriesRequest(
} }
private fun validate() { private fun validate() {
if (title.isBlank()) throw SodaException("시리즈 제목을 입력하세요") if (title.isBlank()) throw SodaException(messageKey = "creator.admin.series.title_required")
if (introduction.isBlank()) throw SodaException("시리즈 소개를 입력하세요") if (introduction.isBlank()) throw SodaException(messageKey = "creator.admin.series.introduction_required")
if (keyword.isBlank()) throw SodaException("시리즈를 설명할 수 있는 키워드를 입력하세요") if (keyword.isBlank()) throw SodaException(messageKey = "creator.admin.series.keyword_required")
if (genreId <= 0) throw SodaException("올바른 장르를 선택하세요") if (genreId <= 0) throw SodaException(messageKey = "creator.admin.series.genre_required")
if (publishedDaysOfWeek.isEmpty()) throw SodaException("시리즈 연재요일을 선택하세요") if (publishedDaysOfWeek.isEmpty()) throw SodaException(messageKey = "creator.admin.series.published_days_required")
if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM) && publishedDaysOfWeek.size > 1) { if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM) && publishedDaysOfWeek.size > 1) {
throw SodaException("랜덤과 연재요일 동시에 선택할 수 없습니다.") throw SodaException(messageKey = "creator.admin.series.published_days_random_exclusive")
} }
} }
} }

View File

@@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.creator.admin.content.series.content.AddingContentToTheSeriesRequest import kr.co.vividnext.sodalive.creator.admin.content.series.content.AddingContentToTheSeriesRequest
import kr.co.vividnext.sodalive.creator.admin.content.series.content.RemoveContentToTheSeriesRequest import kr.co.vividnext.sodalive.creator.admin.content.series.content.RemoveContentToTheSeriesRequest
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.Member
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.lang.Nullable import org.springframework.lang.Nullable
@@ -23,16 +25,23 @@ import org.springframework.web.multipart.MultipartFile
@RestController @RestController
@PreAuthorize("hasRole('CREATOR')") @PreAuthorize("hasRole('CREATOR')")
@RequestMapping("/creator-admin/audio-content/series") @RequestMapping("/creator-admin/audio-content/series")
class CreatorAdminContentSeriesController(private val service: CreatorAdminContentSeriesService) { class CreatorAdminContentSeriesController(
private val service: CreatorAdminContentSeriesService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
@PostMapping @PostMapping
fun createSeries( fun createSeries(
@RequestPart("image") image: MultipartFile?, @RequestPart("image") image: MultipartFile?,
@RequestPart("request") requestString: String, @RequestPart("request") requestString: 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.createSeries(image, requestString, member), "시리즈가 생성되었습니다.") ApiResponse.ok(
service.createSeries(image, requestString, member),
messageSource.getMessage("creator.admin.series.created", langContext.lang)
)
} }
@PutMapping @PutMapping
@@ -43,9 +52,12 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
@RequestPart("request") requestString: String, @RequestPart("request") requestString: 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.modifySeries(image, requestString, member), "시리즈가 수정되었습니다.") ApiResponse.ok(
service.modifySeries(image, requestString, member),
messageSource.getMessage("creator.admin.series.updated", langContext.lang)
)
} }
@GetMapping @GetMapping
@@ -53,7 +65,7 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
pageable: Pageable, pageable: Pageable,
@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( ApiResponse.ok(
service.getSeriesList( service.getSeriesList(
@@ -69,7 +81,7 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
@PathVariable id: Long, @PathVariable id: Long,
@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.getDetail(id = id, memberId = member.id!!)) ApiResponse.ok(service.getDetail(id = id, memberId = member.id!!))
} }
@@ -80,7 +92,7 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
@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( ApiResponse.ok(
service.getSeriesContent( service.getSeriesContent(
@@ -97,11 +109,11 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
@RequestBody request: AddingContentToTheSeriesRequest, @RequestBody request: AddingContentToTheSeriesRequest,
@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( ApiResponse.ok(
service.addingContentToTheSeries(request, memberId = member.id!!), service.addingContentToTheSeries(request, memberId = member.id!!),
"콘텐츠가 추가되었습니다." messageSource.getMessage("creator.admin.series.content_added", langContext.lang)
) )
} }
@@ -110,11 +122,11 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
@RequestBody request: RemoveContentToTheSeriesRequest, @RequestBody request: RemoveContentToTheSeriesRequest,
@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( ApiResponse.ok(
service.removeContentInTheSeries(request, memberId = member.id!!), service.removeContentInTheSeries(request, memberId = member.id!!),
"콘텐츠를 삭제하였습니다." messageSource.getMessage("creator.admin.series.content_removed", langContext.lang)
) )
} }
@@ -124,7 +136,7 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
@RequestParam(value = "search_word") searchWord: String, @RequestParam(value = "search_word") searchWord: 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( ApiResponse.ok(
service.searchContentNotInSeries( service.searchContentNotInSeries(
@@ -138,5 +150,8 @@ class CreatorAdminContentSeriesController(private val service: CreatorAdminConte
@PutMapping("/orders") @PutMapping("/orders")
fun updateSeriesOrders( fun updateSeriesOrders(
@RequestBody request: UpdateOrdersRequest @RequestBody request: UpdateOrdersRequest
) = ApiResponse.ok(service.updateSeriesOrders(ids = request.ids), "수정되었습니다.") ) = ApiResponse.ok(
service.updateSeriesOrders(ids = request.ids),
messageSource.getMessage("creator.admin.series.orders_updated", langContext.lang)
)
} }

View File

@@ -45,7 +45,7 @@ class CreatorAdminContentSeriesService(
) { ) {
@Transactional @Transactional
fun createSeries(coverImage: MultipartFile?, requestString: String, member: Member) { fun createSeries(coverImage: MultipartFile?, requestString: String, member: Member) {
if (coverImage == null) throw SodaException("커버이미지를 선택해 주세요.") if (coverImage == null) throw SodaException(messageKey = "creator.admin.series.cover_image_required")
val request = objectMapper.readValue(requestString, CreateSeriesRequest::class.java) val request = objectMapper.readValue(requestString, CreateSeriesRequest::class.java)
val series = request.toSeries() val series = request.toSeries()
@@ -139,11 +139,11 @@ class CreatorAdminContentSeriesService(
request.studio == null && request.studio == null &&
request.isActive == null request.isActive == null
) { ) {
throw SodaException("변경사항이 없습니다.") throw SodaException(messageKey = "creator.admin.series.no_changes")
} }
val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = member.id!!) val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = member.id!!)
?: throw SodaException("잘못된 접근입니다.") ?: throw SodaException(messageKey = "creator.admin.series.invalid_access")
if (coverImage != null) { if (coverImage != null) {
val metadata = ObjectMetadata() val metadata = ObjectMetadata()
@@ -175,7 +175,7 @@ class CreatorAdminContentSeriesService(
request.publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM) && request.publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM) &&
request.publishedDaysOfWeek.size > 1 request.publishedDaysOfWeek.size > 1
) { ) {
throw SodaException("랜덤과 연재요일 동시에 선택할 수 없습니다.") throw SodaException(messageKey = "creator.admin.series.published_days_random_exclusive")
} }
series.publishedDaysOfWeek.clear() series.publishedDaysOfWeek.clear()
@@ -245,7 +245,7 @@ class CreatorAdminContentSeriesService(
fun getDetail(id: Long, memberId: Long): GetCreatorAdminContentSeriesDetailResponse { fun getDetail(id: Long, memberId: Long): GetCreatorAdminContentSeriesDetailResponse {
val series = repository.findByIdAndCreatorId(id = id, creatorId = memberId) val series = repository.findByIdAndCreatorId(id = id, creatorId = memberId)
?: throw SodaException("잘못된 접근입니다.") ?: throw SodaException(messageKey = "creator.admin.series.invalid_access")
return series.toDetailResponse(imageHost = coverImageHost) return series.toDetailResponse(imageHost = coverImageHost)
} }
@@ -271,7 +271,7 @@ class CreatorAdminContentSeriesService(
@Transactional @Transactional
fun addingContentToTheSeries(request: AddingContentToTheSeriesRequest, memberId: Long) { fun addingContentToTheSeries(request: AddingContentToTheSeriesRequest, memberId: Long) {
val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = memberId) val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = memberId)
?: throw SodaException("잘못된 접근입니다.") ?: throw SodaException(messageKey = "creator.admin.series.invalid_access")
val seriesContentList = mutableListOf<SeriesContent>() val seriesContentList = mutableListOf<SeriesContent>()
@@ -288,14 +288,14 @@ class CreatorAdminContentSeriesService(
if (seriesContentList.size > 0) { if (seriesContentList.size > 0) {
series.contentList.addAll(seriesContentList.toSet()) series.contentList.addAll(seriesContentList.toSet())
} else { } else {
throw SodaException("추가된 콘텐츠가 없습니다.") throw SodaException(messageKey = "creator.admin.series.no_content_added")
} }
} }
@Transactional @Transactional
fun removeContentInTheSeries(request: RemoveContentToTheSeriesRequest, memberId: Long) { fun removeContentInTheSeries(request: RemoveContentToTheSeriesRequest, memberId: Long) {
val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = memberId) val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = memberId)
?: throw SodaException("잘못된 접근입니다.") ?: throw SodaException(messageKey = "creator.admin.series.invalid_access")
series.contentList.removeIf { it.content!!.id == request.contentId } series.contentList.removeIf { it.content!!.id == request.contentId }
} }

View File

@@ -17,7 +17,7 @@ class CreatorAdminContentSeriesGenreController(private val service: CreatorAdmin
fun getGenreList( fun getGenreList(
@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.getGenreList(isAdult = member.auth != null)) ApiResponse.ok(service.getGenreList(isAdult = member.auth != null))
} }

View File

@@ -24,7 +24,7 @@ class CreatorAdminMemberController(private val service: CreatorAdminMemberServic
@RequestHeader("Authorization") token: String, @RequestHeader("Authorization") token: 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.logout(token.removePrefix("Bearer "), member.id!!)) ApiResponse.ok(service.logout(token.removePrefix("Bearer "), member.id!!))
} }

Some files were not shown because too many files have changed in this diff Show More