Compare commits
10 Commits
main
...
c5fa260a0d
| Author | SHA1 | Date | |
|---|---|---|---|
| c5fa260a0d | |||
| 412c52e754 | |||
| 8f4544ad71 | |||
| 619ceeea24 | |||
| a2998002e5 | |||
| da9b89a6cf | |||
| 5ee5107364 | |||
| ae2c699748 | |||
| 93ccb666c4 | |||
| edaea84a5b |
@@ -16,6 +16,7 @@ import javax.persistence.Table
|
|||||||
data class CharacterComment(
|
data class CharacterComment(
|
||||||
@Column(columnDefinition = "TEXT", nullable = false)
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
var comment: String,
|
var comment: String,
|
||||||
|
var languageCode: String?,
|
||||||
var isActive: Boolean = true
|
var isActive: Boolean = true
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class CharacterCommentController(
|
|||||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
if (request.comment.isBlank()) throw SodaException("댓글 내용을 입력해주세요.")
|
if (request.comment.isBlank()) throw SodaException("댓글 내용을 입력해주세요.")
|
||||||
|
|
||||||
val id = service.addReply(characterId, commentId, member, request.comment)
|
val id = service.addReply(characterId, commentId, member, request.comment, request.languageCode)
|
||||||
ApiResponse.ok(id)
|
ApiResponse.ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package kr.co.vividnext.sodalive.chat.character.comment
|
|||||||
|
|
||||||
// Request DTOs
|
// Request DTOs
|
||||||
data class CreateCharacterCommentRequest(
|
data class CreateCharacterCommentRequest(
|
||||||
val comment: String
|
val comment: String,
|
||||||
|
val languageCode: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response DTOs
|
// Response DTOs
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package kr.co.vividnext.sodalive.chat.character.comment
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@@ -12,7 +15,8 @@ import java.time.ZoneId
|
|||||||
class CharacterCommentService(
|
class CharacterCommentService(
|
||||||
private val chatCharacterRepository: ChatCharacterRepository,
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
private val commentRepository: CharacterCommentRepository,
|
private val commentRepository: CharacterCommentRepository,
|
||||||
private val reportRepository: CharacterCommentReportRepository
|
private val reportRepository: CharacterCommentReportRepository,
|
||||||
|
private val applicationEventPublisher: ApplicationEventPublisher
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private fun profileUrl(imageHost: String, profileImage: String?): String {
|
private fun profileUrl(imageHost: String, profileImage: String?): String {
|
||||||
@@ -57,20 +61,38 @@ class CharacterCommentService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun addComment(characterId: Long, member: Member, text: String): 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).orElseThrow { SodaException("캐릭터를 찾을 수 없습니다.") }
|
||||||
if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.")
|
if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.")
|
||||||
if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.")
|
if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.")
|
||||||
|
|
||||||
val entity = CharacterComment(comment = text)
|
val entity = CharacterComment(comment = text, languageCode = languageCode)
|
||||||
entity.chatCharacter = character
|
entity.chatCharacter = character
|
||||||
entity.member = member
|
entity.member = member
|
||||||
commentRepository.save(entity)
|
commentRepository.save(entity)
|
||||||
|
|
||||||
|
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
||||||
|
if (languageCode.isNullOrBlank()) {
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
LanguageDetectEvent(
|
||||||
|
id = entity.id!!,
|
||||||
|
query = text,
|
||||||
|
targetType = LanguageDetectTargetType.CHARACTER_COMMENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return entity.id!!
|
return entity.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun addReply(characterId: Long, parentCommentId: Long, member: Member, text: String): Long {
|
fun addReply(
|
||||||
|
characterId: Long,
|
||||||
|
parentCommentId: Long,
|
||||||
|
member: Member,
|
||||||
|
text: String,
|
||||||
|
languageCode: String? = null
|
||||||
|
): Long {
|
||||||
val character = chatCharacterRepository.findById(characterId).orElseThrow { SodaException("캐릭터를 찾을 수 없습니다.") }
|
val character = chatCharacterRepository.findById(characterId).orElseThrow { SodaException("캐릭터를 찾을 수 없습니다.") }
|
||||||
if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.")
|
if (!character.isActive) throw SodaException("비활성화된 캐릭터입니다.")
|
||||||
val parent = commentRepository.findById(parentCommentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") }
|
val parent = commentRepository.findById(parentCommentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") }
|
||||||
@@ -78,11 +100,23 @@ class CharacterCommentService(
|
|||||||
if (!parent.isActive) throw SodaException("비활성화된 댓글입니다.")
|
if (!parent.isActive) throw SodaException("비활성화된 댓글입니다.")
|
||||||
if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.")
|
if (text.isBlank()) throw SodaException("댓글 내용을 입력해주세요.")
|
||||||
|
|
||||||
val entity = CharacterComment(comment = text)
|
val entity = CharacterComment(comment = text, languageCode = languageCode)
|
||||||
entity.chatCharacter = character
|
entity.chatCharacter = character
|
||||||
entity.member = member
|
entity.member = member
|
||||||
entity.parent = parent
|
entity.parent = parent
|
||||||
commentRepository.save(entity)
|
commentRepository.save(entity)
|
||||||
|
|
||||||
|
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
||||||
|
if (languageCode.isNullOrBlank()) {
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
LanguageDetectEvent(
|
||||||
|
id = entity.id!!,
|
||||||
|
query = text,
|
||||||
|
targetType = LanguageDetectTargetType.CHARACTER_COMMENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return entity.id!!
|
return entity.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ data class AudioContent(
|
|||||||
var title: String,
|
var title: String,
|
||||||
@Column(columnDefinition = "TEXT", nullable = false)
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
var detail: String,
|
var detail: String,
|
||||||
|
var languageCode: String?,
|
||||||
var playCount: Long = 0,
|
var playCount: Long = 0,
|
||||||
var price: Int = 0,
|
var price: Int = 0,
|
||||||
var releaseDate: LocalDateTime? = null,
|
var releaseDate: LocalDateTime? = null,
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ class AudioContentService(
|
|||||||
val audioContent = AudioContent(
|
val audioContent = AudioContent(
|
||||||
title = request.title.trim(),
|
title = request.title.trim(),
|
||||||
detail = request.detail.trim(),
|
detail = request.detail.trim(),
|
||||||
|
languageCode = request.languageCode,
|
||||||
price = if (request.price > 0) {
|
price = if (request.price > 0) {
|
||||||
request.price
|
request.price
|
||||||
} else {
|
} else {
|
||||||
@@ -331,6 +332,24 @@ class AudioContentService(
|
|||||||
|
|
||||||
audioContent.content = contentPath
|
audioContent.content = contentPath
|
||||||
|
|
||||||
|
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
||||||
|
if (audioContent.languageCode.isNullOrBlank()) {
|
||||||
|
val papagoQuery = listOf(
|
||||||
|
request.title.trim(),
|
||||||
|
request.detail.trim(),
|
||||||
|
request.tags.trim()
|
||||||
|
)
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.joinToString(" ")
|
||||||
|
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
LanguageDetectEvent(
|
||||||
|
id = audioContent.id!!,
|
||||||
|
query = papagoQuery
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return CreateAudioContentResponse(contentId = audioContent.id!!)
|
return CreateAudioContentResponse(contentId = audioContent.id!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,6 +722,7 @@ class AudioContentService(
|
|||||||
contentId = audioContent.id!!,
|
contentId = audioContent.id!!,
|
||||||
title = audioContent.title,
|
title = audioContent.title,
|
||||||
detail = contentDetail,
|
detail = contentDetail,
|
||||||
|
languageCode = audioContent.languageCode,
|
||||||
coverImageUrl = "$coverImageHost/${audioContent.coverImage!!}",
|
coverImageUrl = "$coverImageHost/${audioContent.coverImage!!}",
|
||||||
contentUrl = audioContentUrl,
|
contentUrl = audioContentUrl,
|
||||||
themeStr = audioContent.theme!!.theme,
|
themeStr = audioContent.theme!!.theme,
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ data class CreateAudioContentRequest(
|
|||||||
val isCommentAvailable: Boolean = false,
|
val isCommentAvailable: Boolean = false,
|
||||||
val isFullDetailVisible: Boolean = true,
|
val isFullDetailVisible: Boolean = true,
|
||||||
val previewStartTime: String? = null,
|
val previewStartTime: String? = null,
|
||||||
val previewEndTime: String? = null
|
val previewEndTime: String? = null,
|
||||||
|
val languageCode: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ data class GetAudioContentDetailResponse(
|
|||||||
val contentId: Long,
|
val contentId: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val detail: String,
|
val detail: String,
|
||||||
|
val languageCode: String?,
|
||||||
val coverImageUrl: String,
|
val coverImageUrl: String,
|
||||||
val contentUrl: String,
|
val contentUrl: String,
|
||||||
val themeStr: String,
|
val themeStr: String,
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package kr.co.vividnext.sodalive.content
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
|
||||||
|
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.http.HttpEntity
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.scheduling.annotation.Async
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.transaction.annotation.Propagation
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.transaction.event.TransactionPhase
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener
|
||||||
|
import org.springframework.util.LinkedMultiValueMap
|
||||||
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 텍스트 기반 데이터(콘텐츠, 댓글 등)에 대해 파파고 언어 감지를 요청하기 위한 이벤트.
|
||||||
|
*/
|
||||||
|
enum class LanguageDetectTargetType {
|
||||||
|
CONTENT,
|
||||||
|
COMMENT,
|
||||||
|
CHARACTER_COMMENT,
|
||||||
|
CREATOR_CHEERS
|
||||||
|
}
|
||||||
|
|
||||||
|
class LanguageDetectEvent(
|
||||||
|
val id: Long,
|
||||||
|
val query: String,
|
||||||
|
val targetType: LanguageDetectTargetType = LanguageDetectTargetType.CONTENT
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PapagoLanguageDetectResponse(
|
||||||
|
val langCode: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class LanguageDetectListener(
|
||||||
|
private val audioContentRepository: AudioContentRepository,
|
||||||
|
private val audioContentCommentRepository: AudioContentCommentRepository,
|
||||||
|
private val characterCommentRepository: CharacterCommentRepository,
|
||||||
|
private val creatorCheersRepository: CreatorCheersRepository,
|
||||||
|
|
||||||
|
@Value("\${cloud.naver.papago-client-id}")
|
||||||
|
private val papagoClientId: String,
|
||||||
|
|
||||||
|
@Value("\${cloud.naver.papago-client-secret}")
|
||||||
|
private val papagoClientSecret: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(LanguageDetectListener::class.java)
|
||||||
|
|
||||||
|
private val restTemplate: RestTemplate = RestTemplate()
|
||||||
|
|
||||||
|
private val papagoDetectUrl = "https://papago.apigw.ntruss.com/langs/v1/dect"
|
||||||
|
|
||||||
|
@Async
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
fun detectLanguage(event: LanguageDetectEvent) {
|
||||||
|
if (event.query.isBlank()) {
|
||||||
|
log.debug("[PapagoLanguageDetect] query is blank. Skip language detection. event={}", event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (event.targetType) {
|
||||||
|
LanguageDetectTargetType.CONTENT -> handleContentLanguageDetect(event)
|
||||||
|
LanguageDetectTargetType.COMMENT -> handleCommentLanguageDetect(event)
|
||||||
|
LanguageDetectTargetType.CHARACTER_COMMENT -> handleCharacterCommentLanguageDetect(event)
|
||||||
|
LanguageDetectTargetType.CREATOR_CHEERS -> handleCreatorCheersLanguageDetect(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleContentLanguageDetect(event: LanguageDetectEvent) {
|
||||||
|
val contentId = event.id
|
||||||
|
|
||||||
|
val audioContent = audioContentRepository.findById(contentId).orElse(null)
|
||||||
|
if (audioContent == null) {
|
||||||
|
log.warn("[PapagoLanguageDetect] AudioContent not found. contentId={}", contentId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
||||||
|
if (!audioContent.languageCode.isNullOrBlank()) {
|
||||||
|
log.debug(
|
||||||
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. contentId={}, languageCode={}",
|
||||||
|
contentId,
|
||||||
|
audioContent.languageCode
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val langCode = requestPapagoLanguageCode(event.query, contentId) ?: return
|
||||||
|
|
||||||
|
audioContent.languageCode = langCode
|
||||||
|
|
||||||
|
// REQUIRES_NEW 트랜잭션 내에서 변경 내용을 저장한다.
|
||||||
|
audioContentRepository.save(audioContent)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[PapagoLanguageDetect] languageCode updated from Papago. contentId={}, langCode={}",
|
||||||
|
contentId,
|
||||||
|
langCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCommentLanguageDetect(event: LanguageDetectEvent) {
|
||||||
|
val commentId = event.id
|
||||||
|
|
||||||
|
val comment = audioContentCommentRepository.findById(commentId).orElse(null)
|
||||||
|
if (comment == null) {
|
||||||
|
log.warn("[PapagoLanguageDetect] AudioContentComment not found. commentId={}", commentId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
||||||
|
if (!comment.languageCode.isNullOrBlank()) {
|
||||||
|
log.debug(
|
||||||
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. commentId={}, languageCode={}",
|
||||||
|
commentId,
|
||||||
|
comment.languageCode
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val langCode = requestPapagoLanguageCode(event.query, commentId) ?: return
|
||||||
|
|
||||||
|
comment.languageCode = langCode
|
||||||
|
audioContentCommentRepository.save(comment)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[PapagoLanguageDetect] languageCode updated from Papago. commentId={}, langCode={}",
|
||||||
|
commentId,
|
||||||
|
langCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCharacterCommentLanguageDetect(event: LanguageDetectEvent) {
|
||||||
|
val commentId = event.id
|
||||||
|
|
||||||
|
val comment = characterCommentRepository.findById(commentId).orElse(null)
|
||||||
|
if (comment == null) {
|
||||||
|
log.warn("[PapagoLanguageDetect] CharacterComment not found. commentId={}", commentId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
||||||
|
if (!comment.languageCode.isNullOrBlank()) {
|
||||||
|
log.debug(
|
||||||
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. " +
|
||||||
|
"characterCommentId={}, languageCode={}",
|
||||||
|
commentId,
|
||||||
|
comment.languageCode
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val langCode = requestPapagoLanguageCode(event.query, commentId) ?: return
|
||||||
|
|
||||||
|
comment.languageCode = langCode
|
||||||
|
characterCommentRepository.save(comment)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[PapagoLanguageDetect] languageCode updated from Papago. characterCommentId={}, langCode={}",
|
||||||
|
commentId,
|
||||||
|
langCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCreatorCheersLanguageDetect(event: LanguageDetectEvent) {
|
||||||
|
val cheersId = event.id
|
||||||
|
|
||||||
|
val cheers = creatorCheersRepository.findById(cheersId).orElse(null)
|
||||||
|
if (cheers == null) {
|
||||||
|
log.warn("[PapagoLanguageDetect] CreatorCheers not found. cheersId={}", cheersId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
||||||
|
if (!cheers.languageCode.isNullOrBlank()) {
|
||||||
|
log.debug(
|
||||||
|
"[PapagoLanguageDetect] languageCode already set. Skip language detection. cheersId={}, languageCode={}",
|
||||||
|
cheersId,
|
||||||
|
cheers.languageCode
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val langCode = requestPapagoLanguageCode(event.query, cheersId) ?: return
|
||||||
|
|
||||||
|
cheers.languageCode = langCode
|
||||||
|
creatorCheersRepository.save(cheers)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[PapagoLanguageDetect] languageCode updated from Papago. cheersId={}, langCode={}",
|
||||||
|
cheersId,
|
||||||
|
langCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestPapagoLanguageCode(query: String, targetIdForLog: Long): String? {
|
||||||
|
return try {
|
||||||
|
val headers = HttpHeaders().apply {
|
||||||
|
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
|
set("X-NCP-APIGW-API-KEY-ID", papagoClientId)
|
||||||
|
set("X-NCP-APIGW-API-KEY", papagoClientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = LinkedMultiValueMap<String, String>().apply {
|
||||||
|
// 파파고 스펙에 따라 query 파라미터에 텍스트를 공백으로 구분하여 전달
|
||||||
|
add("query", query)
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestEntity = HttpEntity(body, headers)
|
||||||
|
|
||||||
|
val response = restTemplate.postForEntity(
|
||||||
|
papagoDetectUrl,
|
||||||
|
requestEntity,
|
||||||
|
PapagoLanguageDetectResponse::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.statusCode.is2xxSuccessful) {
|
||||||
|
log.warn(
|
||||||
|
"[PapagoLanguageDetect] Non-success status from Papago. status={}, targetId={}",
|
||||||
|
response.statusCode,
|
||||||
|
targetIdForLog
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val langCode = response.body?.langCode?.takeIf { it.isNotBlank() }
|
||||||
|
if (langCode == null) {
|
||||||
|
log.warn(
|
||||||
|
"[PapagoLanguageDetect] langCode is null or blank in Papago response. targetId={}",
|
||||||
|
targetIdForLog
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
langCode
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
// 언어 감지는 부가 기능이므로, 실패 시 예외를 전파하지 않고 로그만 남긴다.
|
||||||
|
log.error(
|
||||||
|
"[PapagoLanguageDetect] Failed to detect language via Papago. targetId={}",
|
||||||
|
targetIdForLog,
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import javax.persistence.Table
|
|||||||
data class AudioContentComment(
|
data class AudioContentComment(
|
||||||
@Column(columnDefinition = "TEXT", nullable = false)
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
var comment: String,
|
var comment: String,
|
||||||
|
var languageCode: String?,
|
||||||
@Column(nullable = true)
|
@Column(nullable = true)
|
||||||
var donationCan: Int? = null,
|
var donationCan: Int? = null,
|
||||||
val isSecret: Boolean = false,
|
val isSecret: Boolean = false,
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ class AudioContentCommentController(
|
|||||||
audioContentId = request.contentId,
|
audioContentId = request.contentId,
|
||||||
parentId = request.parentId,
|
parentId = request.parentId,
|
||||||
isSecret = request.isSecret,
|
isSecret = request.isSecret,
|
||||||
member = member
|
member = member,
|
||||||
|
languageCode = request.languageCode
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.content.comment
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||||
|
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
|
||||||
@@ -32,7 +34,8 @@ class AudioContentCommentService(
|
|||||||
comment: String,
|
comment: String,
|
||||||
audioContentId: Long,
|
audioContentId: Long,
|
||||||
parentId: Long? = null,
|
parentId: Long? = null,
|
||||||
isSecret: Boolean = false
|
isSecret: Boolean = false,
|
||||||
|
languageCode: String?
|
||||||
): Long {
|
): Long {
|
||||||
val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId)
|
val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId)
|
||||||
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
||||||
@@ -50,7 +53,7 @@ class AudioContentCommentService(
|
|||||||
throw SodaException("콘텐츠 구매 후 비밀댓글을 등록할 수 있습니다.")
|
throw SodaException("콘텐츠 구매 후 비밀댓글을 등록할 수 있습니다.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val audioContentComment = AudioContentComment(comment = comment, isSecret = isSecret)
|
val audioContentComment = AudioContentComment(comment = comment, languageCode = languageCode, isSecret = isSecret)
|
||||||
audioContentComment.audioContent = audioContent
|
audioContentComment.audioContent = audioContent
|
||||||
audioContentComment.member = member
|
audioContentComment.member = member
|
||||||
|
|
||||||
@@ -85,6 +88,17 @@ class AudioContentCommentService(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
||||||
|
if (languageCode.isNullOrBlank()) {
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
LanguageDetectEvent(
|
||||||
|
id = savedContentComment.id!!,
|
||||||
|
query = comment,
|
||||||
|
targetType = LanguageDetectTargetType.COMMENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return savedContentComment.id!!
|
return savedContentComment.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ data class RegisterCommentRequest(
|
|||||||
val comment: String,
|
val comment: String,
|
||||||
val contentId: Long,
|
val contentId: Long,
|
||||||
val parentId: Long?,
|
val parentId: Long?,
|
||||||
val isSecret: Boolean = false
|
val isSecret: Boolean = false,
|
||||||
|
val languageCode: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ data class AudioContentDonationRequest(
|
|||||||
val contentId: Long,
|
val contentId: Long,
|
||||||
val donationCan: Int,
|
val donationCan: Int,
|
||||||
val comment: String,
|
val comment: String,
|
||||||
val container: String
|
val container: String,
|
||||||
|
val languageCode: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import kr.co.vividnext.sodalive.can.payment.CanPaymentService
|
|||||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
||||||
import kr.co.vividnext.sodalive.content.comment.AudioContentComment
|
import kr.co.vividnext.sodalive.content.comment.AudioContentComment
|
||||||
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@@ -14,7 +17,8 @@ import org.springframework.transaction.annotation.Transactional
|
|||||||
class AudioContentDonationService(
|
class AudioContentDonationService(
|
||||||
private val canPaymentService: CanPaymentService,
|
private val canPaymentService: CanPaymentService,
|
||||||
private val queryRepository: AudioContentRepository,
|
private val queryRepository: AudioContentRepository,
|
||||||
private val commentRepository: AudioContentCommentRepository
|
private val commentRepository: AudioContentCommentRepository,
|
||||||
|
private val applicationEventPublisher: ApplicationEventPublisher
|
||||||
) {
|
) {
|
||||||
@Transactional
|
@Transactional
|
||||||
fun donation(request: AudioContentDonationRequest, member: Member) {
|
fun donation(request: AudioContentDonationRequest, member: Member) {
|
||||||
@@ -34,10 +38,23 @@ class AudioContentDonationService(
|
|||||||
|
|
||||||
val audioContentComment = AudioContentComment(
|
val audioContentComment = AudioContentComment(
|
||||||
comment = request.comment,
|
comment = request.comment,
|
||||||
|
languageCode = request.languageCode,
|
||||||
donationCan = request.donationCan
|
donationCan = request.donationCan
|
||||||
)
|
)
|
||||||
audioContentComment.audioContent = audioContent
|
audioContentComment.audioContent = audioContent
|
||||||
audioContentComment.member = member
|
audioContentComment.member = member
|
||||||
commentRepository.save(audioContentComment)
|
|
||||||
|
val savedComment = commentRepository.save(audioContentComment)
|
||||||
|
|
||||||
|
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
||||||
|
if (request.languageCode.isNullOrBlank()) {
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
LanguageDetectEvent(
|
||||||
|
id = savedComment.id!!,
|
||||||
|
query = request.comment,
|
||||||
|
targetType = LanguageDetectTargetType.COMMENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.explorer
|
|||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentService
|
import kr.co.vividnext.sodalive.content.AudioContentService
|
||||||
import kr.co.vividnext.sodalive.content.ContentType
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||||
|
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
||||||
import kr.co.vividnext.sodalive.content.SortType
|
import kr.co.vividnext.sodalive.content.SortType
|
||||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
||||||
import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponse
|
import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponse
|
||||||
@@ -441,7 +443,7 @@ class ExplorerService(
|
|||||||
val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = request.creatorId)
|
val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = request.creatorId)
|
||||||
if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 팬토크 작성이 제한됩니다.")
|
if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 팬토크 작성이 제한됩니다.")
|
||||||
|
|
||||||
val cheers = CreatorCheers(cheers = request.content)
|
val cheers = CreatorCheers(cheers = request.content, languageCode = request.languageCode)
|
||||||
cheers.member = member
|
cheers.member = member
|
||||||
cheers.creator = creator
|
cheers.creator = creator
|
||||||
|
|
||||||
@@ -456,6 +458,17 @@ class ExplorerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cheersRepository.save(cheers)
|
cheersRepository.save(cheers)
|
||||||
|
|
||||||
|
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
||||||
|
if (request.languageCode.isNullOrBlank()) {
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
LanguageDetectEvent(
|
||||||
|
id = cheers.id!!,
|
||||||
|
query = request.content,
|
||||||
|
targetType = LanguageDetectTargetType.CREATOR_CHEERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCreatorProfileCheers(
|
fun getCreatorProfileCheers(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import javax.persistence.OneToMany
|
|||||||
data class CreatorCheers(
|
data class CreatorCheers(
|
||||||
@Column(columnDefinition = "TEXT", nullable = false)
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
var cheers: String,
|
var cheers: String,
|
||||||
|
var languageCode: String?,
|
||||||
var isActive: Boolean = true
|
var isActive: Boolean = true
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ package kr.co.vividnext.sodalive.explorer.profile
|
|||||||
data class PostWriteCheersRequest(
|
data class PostWriteCheersRequest(
|
||||||
val parentId: Long? = null,
|
val parentId: Long? = null,
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
val content: String
|
val content: String,
|
||||||
|
val languageCode: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ google:
|
|||||||
webClientId: ${GOOGLE_WEB_CLIENT_ID}
|
webClientId: ${GOOGLE_WEB_CLIENT_ID}
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
|
naver:
|
||||||
|
papagoClientId: ${NCLOUD_PAPAGO_CLIENT_ID}
|
||||||
|
papagoClientSecret: ${NCLOUD_PAPAGO_CLIENT_SECRET}
|
||||||
aws:
|
aws:
|
||||||
credentials:
|
credentials:
|
||||||
accessKey: ${APP_AWS_ACCESS_KEY}
|
accessKey: ${APP_AWS_ACCESS_KEY}
|
||||||
|
|||||||
Reference in New Issue
Block a user