Compare commits
367 Commits
143ba2fbb2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 587f3d6b58 | |||
| 9b6167d46d | |||
| 008ee3b4e5 | |||
| 3a57ad23bb | |||
| 729552335a | |||
| 02ae507c87 | |||
| 5818abf69d | |||
| ee403915f0 | |||
| 1a660088de | |||
| 5196c80ca8 | |||
| c9c09c2998 | |||
| 3ea33c4c7b | |||
| 451a1aa4f2 | |||
| 90555fd34f | |||
| 0dc430b098 | |||
| 1f2103c7fa | |||
| 062c17c51e | |||
| de169b79a1 | |||
| aa24de0a5a | |||
| e5937d573a | |||
| 6da86e12bd | |||
| 9049022a74 | |||
| 7b6f3a7a5f | |||
| 53e9678efa | |||
| e4f547fa92 | |||
| b69756ef81 | |||
| 1a3a9149a2 | |||
| ce120a6d5d | |||
| 08b5fd23ab | |||
| eb18e2d009 | |||
| a27852ed44 | |||
| c7925c1706 | |||
| be59bd7e89 | |||
| 51ce143fc2 | |||
| 89eb11f808 | |||
| 30d89987a4 | |||
| 7959d3e5ed | |||
| 1e29573ef7 | |||
| cc2f533dc6 | |||
| 32b0c19f9d | |||
| 9af2d768e8 | |||
| 5677824cde | |||
| e8f1bc09f9 | |||
| d1a936d55b | |||
| dc97eaa835 | |||
| dcbe57806c | |||
| b14438cc15 | |||
| b27d3bd5c6 | |||
| 03ebc9cfe9 | |||
| 24841b9850 | |||
| d35a3d1a8c | |||
| 60c4e0b528 | |||
| 84f33d1bc2 | |||
| c4e1709b99 | |||
| e7a5fd5819 | |||
| 4bde03643c | |||
| 1bc52b56af | |||
| 9c33fd93f7 | |||
| 3c087bc275 | |||
| 8ad13c289e | |||
| 7577f48a09 | |||
| 0251906964 | |||
| 2723a5f134 | |||
| c3c60605fd | |||
| 238f704b22 | |||
| 5639d8ac8e | |||
| 9aac591591 | |||
| ffa8e5aebb | |||
| cbbfe014cc | |||
| 83028f7817 | |||
| 70d1795557 | |||
| 8c6c681424 | |||
| 50bc9f4ff3 | |||
| f00ea03fad | |||
| f22e7b9ad1 | |||
| c7ec95f4bb | |||
| 229e7a8ccc | |||
| 3c616474ff | |||
| 56eb6b3ce3 | |||
| 545836d43c | |||
| 219f83dec0 | |||
| a76a841238 | |||
| c26680de84 | |||
| 8fffad9d3a | |||
| f4f0f203a2 | |||
| b7196f5a0c | |||
| 5d33a18890 | |||
| 96186a1a50 | |||
| bc8bc479d1 | |||
| 47595b1291 | |||
| 01a88964df | |||
| 3a2b77379f | |||
| dc4e5f75cd | |||
| d0178d551c | |||
| 827333108d | |||
| 587b90bd27 | |||
| 4dc20c5e90 | |||
| ac25782f2b | |||
| 20437d56e7 | |||
| f0b412828a | |||
| 367faac5c3 | |||
| 84deaaa970 | |||
| a2b39466c2 | |||
| 03586c4005 | |||
| 6ea69e1510 | |||
| 553c6dc539 | |||
| 6cc22f5b6d | |||
| 9103d67cc1 | |||
| 25083fb0e4 | |||
| d2dc045255 | |||
| b8621dfbb0 | |||
| 93633940dd | |||
| b6f5325351 | |||
| 7c32c08f1f | |||
| 1d268da08d | |||
| 797666ae0d | |||
| dcf470997e | |||
| 0974d1dbf8 | |||
| 12a35db6cd | |||
| 9abbb05ad8 | |||
| 1ecaf69b0b | |||
| e334d1e5d9 | |||
| b735e861d0 | |||
| 4eb433d372 | |||
| 2416ae61f3 | |||
| 01fb336985 | |||
| b6af88a732 | |||
| 58a2a17d6d | |||
| 79f5a0f520 | |||
| 7f6c0f7f04 | |||
| f658df4dca | |||
| 9d43b8e23a | |||
| 4270aef79b | |||
| 1c0dc82d44 | |||
| c1e325aadf | |||
| cec87da69d | |||
| f68f24cb2c | |||
| ed094347fc | |||
| b8afdffbe1 | |||
| f6ba79f31c | |||
| 5f3b1663d2 | |||
| 66e786b4bb | |||
| f671114574 | |||
| ce37060d94 | |||
| 7d19a4d184 | |||
| 22f28a2f8a | |||
| ceef9ca979 | |||
| efe8f4f939 | |||
| ba692a1195 | |||
| d732bad042 | |||
| 4c935c3bee | |||
| c160dd791f | |||
| 23cd1b4601 | |||
| 031fc8ba1b | |||
| c6853289ad | |||
| 2497bb69bc | |||
| a58a67e0a2 | |||
| 4315fe12a5 | |||
| 42f10a8899 | |||
| 1e4b47f989 | |||
| ff255dbfae | |||
| dbe9b72feb | |||
| 95a714b391 | |||
| 28f58c7f56 | |||
| 8bd46d8f21 | |||
| e1bb8e54ed | |||
| 1de705b063 | |||
| f6926ad356 | |||
| 2cdbbb1b37 | |||
| 4dce8c8f03 | |||
| 97a5bace6f | |||
| d4d51ec48f | |||
| fb91398462 | |||
| 105dadd798 | |||
| 2abf2837d3 | |||
| 422aa67af6 | |||
| 7fffab6985 | |||
| 5a4be3d2c1 | |||
| f39a7681db | |||
| c60a7580ba | |||
| 97edb56edc | |||
| 6ebca8d22b | |||
| 95371ad934 | |||
| 2c176825fd | |||
| fae7de48d3 | |||
| b8230646a2 | |||
| 43279541dd | |||
| b4791977c1 | |||
| ef917ecc25 | |||
| a93faad951 | |||
| fd001d24d3 | |||
| 7aa5884797 | |||
| 5b237a1547 | |||
| 2e37990d87 | |||
| dd07d724a8 | |||
| 03ce8618e7 | |||
| db1a7a7fd6 | |||
| 36a82d7f53 | |||
| 3a34401113 | |||
| 9927268330 | |||
| c45c97e29d | |||
| c64a315226 | |||
| a4cafca6ab | |||
| 46284a0660 | |||
| 05df86e15a | |||
| 8b433027e2 | |||
| 5bd4ff7610 | |||
| d693c397ea | |||
| 1d8d1ec9a5 | |||
| 5e491f11ee | |||
| 7cedea06ac | |||
| 2e5f750e50 | |||
| 20289cad10 | |||
| e0d64c31c7 | |||
| 8c1b95dc97 | |||
| fb5641343e | |||
| 87765941eb | |||
| 1809862c16 | |||
| 300f784f7d | |||
| 67a045eae6 | |||
| 2a79903a28 | |||
| d3222ce083 | |||
| 406a421742 | |||
| 10bf728faf | |||
| 607617747c | |||
| f0a69eb1a2 | |||
| 6b307a6e17 | |||
| 08d08a934a | |||
| c500c12668 | |||
| 62060adeba | |||
| b2fc75edb8 | |||
| a999dd2085 | |||
| 49f95ab100 | |||
| 1a84d5b30c | |||
| 3b65050632 | |||
| d0df31674c | |||
| 1fe88402e2 | |||
| 67097696e6 | |||
| 8e7e77067a | |||
| 9899390b61 | |||
| 80c476a908 | |||
| 59da1d6e49 | |||
| 5aef7dac33 | |||
| faf7aa06b6 | |||
| 38ef6e5583 | |||
| c0b15b5d94 | |||
| 2cfc067ea1 | |||
| a91db4f956 | |||
| 8a09780a02 | |||
| 45e8ec6505 | |||
| 4554b85914 | |||
| 8aa79c4a9c | |||
| c8d3210b57 | |||
| 2282a49563 | |||
| b82fdfb2c8 | |||
| 2d17eac199 | |||
| e482bc3aad | |||
| ec022b74d1 | |||
| dc42c09ce3 | |||
| 046a34d2a4 | |||
| 9ff6ec1888 | |||
| d2950106ec | |||
| 962f800d2e | |||
| 962107e507 | |||
| 039bd11963 | |||
| 5c250ea4ae | |||
| e3405bcec6 | |||
| 0fd1c2235f | |||
| b20c29b022 | |||
| 12d5dcd298 | |||
| 2c305dc6c6 | |||
| 62f76f7433 | |||
| 858ce524f9 | |||
| 3795fb4a40 | |||
| 0c01aeec50 | |||
| 892206744d | |||
| 9e2c1474db | |||
| 16328f73d9 | |||
| e0d4f53cf4 | |||
| e09a59c5b4 | |||
| 049e654535 | |||
| c927dc4ecd | |||
| fe4ecd0ad8 | |||
| 78d476fe80 | |||
| a11c8465d5 | |||
| 366304a9b7 | |||
| 4356663688 | |||
| 26b55e6fcf | |||
| 0d743f7204 | |||
| 6cbe113b3e | |||
| 6409b69d6c | |||
| c5164c76fc | |||
| baade8e138 | |||
| b848d6b4e0 | |||
| d8139d2ab0 | |||
| e96d8f7469 | |||
| 2acffd8afc | |||
| 3c8e72073c | |||
| 724d7a9d9b | |||
| 2da3b0db78 | |||
| 685ad7afaf | |||
| 264cf75964 | |||
| c773dbc7b5 | |||
| 37cbc64f52 | |||
| cb1dde17bb | |||
| c29988acf4 | |||
| eadbf56dae | |||
| 4b3b455135 | |||
| e6ac177396 | |||
| 3d0e29003f | |||
| 78b9b00f77 | |||
| 0ee7faa551 | |||
| e5fdced681 | |||
| afb99fef64 | |||
| 7dfaa36024 | |||
| 0496f665aa | |||
| 0d19e1be74 | |||
| 4aff0111aa | |||
| 63b3ba2bb2 | |||
| 7444b41f60 | |||
| 8e90dbc8b6 | |||
| 9f70722521 | |||
| 52fae596fa | |||
| ccb67957bc | |||
| fb82538d0d | |||
| 72ee39612e | |||
| 51fd5408dc | |||
| 3fae40fbef | |||
| 0745890af0 | |||
| 4abe1730a7 | |||
| 626f0e6989 | |||
| 9f42d9d173 | |||
| f90a93c4bc | |||
| 8000ad6c6a | |||
| 1f1f1bea1a | |||
| d95460c7cd | |||
| a3d93d4b08 | |||
| 07a92af982 | |||
| f4618877d4 | |||
| 2b914fd222 | |||
| 109e42a5a3 | |||
| fa515ad39c | |||
| f09673a795 | |||
| f71536c614 | |||
| 7bdddc7ae8 | |||
| aa8926a624 | |||
| be71e59be2 | |||
| 4d7753378f | |||
| 60257c4ef4 | |||
| 1e0b79bf62 | |||
| 6883434d0d | |||
| eda2193e64 | |||
| 99bf829c88 | |||
| 5feafe1b48 | |||
| c9292b7d04 | |||
| ae7e1a91c1 | |||
| 3e1887e0d1 | |||
| 474646db47 | |||
| 56f7b6c449 | |||
| 76b2b5f7e3 | |||
| e918d809eb | |||
| 7af059e543 | |||
| 897726e1ec | |||
| 8b98a2dd07 | |||
| cca75420f0 | |||
| 86c627ed1d | |||
| d55514e3a7 |
@@ -13,11 +13,8 @@ import kr.co.vividnext.sodalive.chat.character.CharacterType
|
|||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||||
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.LanguageDetectEvent
|
|
||||||
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
|
||||||
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.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
@@ -43,7 +40,6 @@ class AdminChatCharacterController(
|
|||||||
private val adminService: AdminChatCharacterService,
|
private val adminService: AdminChatCharacterService,
|
||||||
private val s3Uploader: S3Uploader,
|
private val s3Uploader: S3Uploader,
|
||||||
private val originalWorkService: AdminOriginalWorkService,
|
private val originalWorkService: AdminOriginalWorkService,
|
||||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
|
||||||
|
|
||||||
@Value("\${weraser.api-key}")
|
@Value("\${weraser.api-key}")
|
||||||
private val apiKey: String,
|
private val apiKey: String,
|
||||||
@@ -169,18 +165,6 @@ class AdminChatCharacterController(
|
|||||||
originalWorkService.assignOneCharacter(request.originalWorkId, chatCharacter.id!!)
|
originalWorkService.assignOneCharacter(request.originalWorkId, chatCharacter.id!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
|
||||||
// 언어 감지에 사용할 내용은 chatCharacter.description 만 사용한다.
|
|
||||||
if (chatCharacter.languageCode.isNullOrBlank() && chatCharacter.description.isNotBlank()) {
|
|
||||||
applicationEventPublisher.publishEvent(
|
|
||||||
LanguageDetectEvent(
|
|
||||||
id = chatCharacter.id!!,
|
|
||||||
query = chatCharacter.description,
|
|
||||||
targetType = LanguageDetectTargetType.CHARACTER
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiResponse.ok(null)
|
ApiResponse.ok(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class HomeController(private val service: HomeService) {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
fun fetchData(
|
fun fetchData(
|
||||||
@RequestParam timezone: String,
|
@RequestParam timezone: String,
|
||||||
@RequestParam(required = false) languageCode: String? = null,
|
|
||||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||||
@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?
|
||||||
@@ -25,7 +24,6 @@ class HomeController(private val service: HomeService) {
|
|||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
service.fetchData(
|
service.fetchData(
|
||||||
timezone = timezone,
|
timezone = timezone,
|
||||||
languageCode = languageCode,
|
|
||||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||||
contentType = contentType ?: ContentType.ALL,
|
contentType = contentType ?: ContentType.ALL,
|
||||||
member
|
member
|
||||||
@@ -36,7 +34,6 @@ class HomeController(private val service: HomeService) {
|
|||||||
@GetMapping("/latest-content")
|
@GetMapping("/latest-content")
|
||||||
fun getLatestContentByTheme(
|
fun getLatestContentByTheme(
|
||||||
@RequestParam("theme") theme: String,
|
@RequestParam("theme") theme: String,
|
||||||
@RequestParam(required = false) languageCode: String? = null,
|
|
||||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||||
@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?
|
||||||
@@ -44,7 +41,6 @@ class HomeController(private val service: HomeService) {
|
|||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
service.getLatestContentByTheme(
|
service.getLatestContentByTheme(
|
||||||
theme = theme,
|
theme = theme,
|
||||||
languageCode = languageCode,
|
|
||||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||||
contentType = contentType ?: ContentType.ALL,
|
contentType = contentType ?: ContentType.ALL,
|
||||||
member
|
member
|
||||||
@@ -74,15 +70,13 @@ class HomeController(private val service: HomeService) {
|
|||||||
fun getRecommendContents(
|
fun getRecommendContents(
|
||||||
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
@RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null,
|
||||||
@RequestParam("contentType", required = false) contentType: ContentType? = null,
|
@RequestParam("contentType", required = false) contentType: ContentType? = null,
|
||||||
@RequestParam(required = false) languageCode: String? = null,
|
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = run {
|
) = run {
|
||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
service.getRecommendContentList(
|
service.getRecommendContentList(
|
||||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||||
contentType = contentType ?: ContentType.ALL,
|
contentType = contentType ?: ContentType.ALL,
|
||||||
member = member,
|
member = member
|
||||||
languageCode = languageCode
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.api.home
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.audition.AuditionService
|
import kr.co.vividnext.sodalive.audition.AuditionService
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
||||||
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
|
||||||
@@ -12,7 +11,6 @@ import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationServic
|
|||||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
||||||
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
||||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService
|
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||||
import kr.co.vividnext.sodalive.event.GetEventResponse
|
import kr.co.vividnext.sodalive.event.GetEventResponse
|
||||||
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||||
@@ -49,9 +47,6 @@ class HomeService(
|
|||||||
private val rankingRepository: RankingRepository,
|
private val rankingRepository: RankingRepository,
|
||||||
private val explorerQueryRepository: ExplorerQueryRepository,
|
private val explorerQueryRepository: ExplorerQueryRepository,
|
||||||
|
|
||||||
private val contentTranslationRepository: ContentTranslationRepository,
|
|
||||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
|
||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
) {
|
) {
|
||||||
@@ -62,7 +57,6 @@ class HomeService(
|
|||||||
|
|
||||||
fun fetchData(
|
fun fetchData(
|
||||||
timezone: String,
|
timezone: String,
|
||||||
languageCode: String?,
|
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
member: Member?
|
member: Member?
|
||||||
@@ -117,37 +111,6 @@ class HomeService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* latestContentList 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 latestContentList의 title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedLatestContentList = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val contentIds = latestContentList.map { it.contentId }
|
|
||||||
|
|
||||||
if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
latestContentList.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
latestContentList
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
latestContentList
|
|
||||||
}
|
|
||||||
|
|
||||||
val eventBannerList = GetEventResponse(
|
val eventBannerList = GetEventResponse(
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
eventList = emptyList()
|
eventList = emptyList()
|
||||||
@@ -177,38 +140,6 @@ class HomeService(
|
|||||||
// 인기 캐릭터 조회
|
// 인기 캐릭터 조회
|
||||||
val popularCharacters = characterService.getPopularCharacters()
|
val popularCharacters = characterService.getPopularCharacters()
|
||||||
|
|
||||||
/**
|
|
||||||
* popularCharacters 캐릭터 이름 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 characterId 매핑하여 popularCharacters의 캐릭터 이름을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedPopularCharacters = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val characterIds = popularCharacters.map { it.characterId }
|
|
||||||
|
|
||||||
if (characterIds.isNotEmpty()) {
|
|
||||||
val translations = aiCharacterTranslationRepository
|
|
||||||
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
|
|
||||||
.associateBy { it.characterId }
|
|
||||||
|
|
||||||
popularCharacters.map { character ->
|
|
||||||
val translatedName = translations[character.characterId]?.renderedPayload?.name
|
|
||||||
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
|
|
||||||
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
|
|
||||||
character
|
|
||||||
} else {
|
|
||||||
character.copy(name = translatedName, description = translatedDesc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
popularCharacters
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
popularCharacters
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentDateTime = LocalDateTime.now()
|
val currentDateTime = LocalDateTime.now()
|
||||||
val startDate = currentDateTime
|
val startDate = currentDateTime
|
||||||
.withHour(15)
|
.withHour(15)
|
||||||
@@ -228,81 +159,12 @@ class HomeService(
|
|||||||
sort = ContentRankingSortType.REVENUE
|
sort = ContentRankingSortType.REVENUE
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* contentRanking 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 contentRanking title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedContentRanking = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val contentIds = contentRanking.map { it.contentId }
|
|
||||||
|
|
||||||
if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
contentRanking.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentRanking
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentRanking
|
|
||||||
}
|
|
||||||
|
|
||||||
val recommendChannelList = recommendChannelService.getRecommendChannel(
|
val recommendChannelList = recommendChannelService.getRecommendChannel(
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType
|
contentType = contentType
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* recommendChannelList의 콘텐츠 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 recommendChannelList의 콘텐츠 title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedRecommendChannelList = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val contentIds = recommendChannelList
|
|
||||||
.flatMap { it.contentList }
|
|
||||||
.map { it.contentId }
|
|
||||||
.distinct()
|
|
||||||
|
|
||||||
if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
recommendChannelList.map { channel ->
|
|
||||||
val translatedContentList = channel.contentList.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.copy(contentList = translatedContentList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recommendChannelList
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recommendChannelList
|
|
||||||
}
|
|
||||||
|
|
||||||
val freeContentList = contentService.getLatestContentByTheme(
|
val freeContentList = contentService.getLatestContentByTheme(
|
||||||
theme = contentThemeService.getActiveThemeOfContent(
|
theme = contentThemeService.getActiveThemeOfContent(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
@@ -321,37 +183,6 @@ class HomeService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* freeContentList 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 freeContentList title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedFreeContentList = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val contentIds = freeContentList.map { it.contentId }
|
|
||||||
|
|
||||||
if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
freeContentList.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
freeContentList
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
freeContentList
|
|
||||||
}
|
|
||||||
|
|
||||||
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
|
// 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용)
|
||||||
val pointAvailableContentList = contentService.getLatestContentByTheme(
|
val pointAvailableContentList = contentService.getLatestContentByTheme(
|
||||||
theme = emptyList(),
|
theme = emptyList(),
|
||||||
@@ -368,37 +199,6 @@ class HomeService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* pointAvailableContentList 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 pointAvailableContentList title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedPointAvailableContentList = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val contentIds = pointAvailableContentList.map { it.contentId }
|
|
||||||
|
|
||||||
if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
pointAvailableContentList.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pointAvailableContentList
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pointAvailableContentList
|
|
||||||
}
|
|
||||||
|
|
||||||
val curationList = curationService.getContentCurationList(
|
val curationList = curationService.getContentCurationList(
|
||||||
tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용
|
tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
@@ -410,22 +210,21 @@ class HomeService(
|
|||||||
liveList = liveList,
|
liveList = liveList,
|
||||||
creatorRanking = creatorRanking,
|
creatorRanking = creatorRanking,
|
||||||
latestContentThemeList = latestContentThemeList,
|
latestContentThemeList = latestContentThemeList,
|
||||||
latestContentList = translatedLatestContentList,
|
latestContentList = latestContentList,
|
||||||
bannerList = bannerList,
|
bannerList = bannerList,
|
||||||
eventBannerList = eventBannerList,
|
eventBannerList = eventBannerList,
|
||||||
originalAudioDramaList = originalAudioDramaList,
|
originalAudioDramaList = originalAudioDramaList,
|
||||||
auditionList = auditionList,
|
auditionList = auditionList,
|
||||||
dayOfWeekSeriesList = dayOfWeekSeriesList,
|
dayOfWeekSeriesList = dayOfWeekSeriesList,
|
||||||
popularCharacters = translatedPopularCharacters,
|
popularCharacters = popularCharacters,
|
||||||
contentRanking = translatedContentRanking,
|
contentRanking = contentRanking,
|
||||||
recommendChannelList = translatedRecommendChannelList,
|
recommendChannelList = recommendChannelList,
|
||||||
freeContentList = translatedFreeContentList,
|
freeContentList = freeContentList,
|
||||||
pointAvailableContentList = translatedPointAvailableContentList,
|
pointAvailableContentList = pointAvailableContentList,
|
||||||
recommendContentList = getRecommendContentList(
|
recommendContentList = getRecommendContentList(
|
||||||
isAdultContentVisible = isAdultContentVisible,
|
isAdultContentVisible = isAdultContentVisible,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
member = member,
|
member = member
|
||||||
languageCode = languageCode
|
|
||||||
),
|
),
|
||||||
curationList = curationList
|
curationList = curationList
|
||||||
)
|
)
|
||||||
@@ -433,7 +232,6 @@ class HomeService(
|
|||||||
|
|
||||||
fun getLatestContentByTheme(
|
fun getLatestContentByTheme(
|
||||||
theme: String,
|
theme: String,
|
||||||
languageCode: String?,
|
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
member: Member?
|
member: Member?
|
||||||
@@ -451,7 +249,7 @@ class HomeService(
|
|||||||
listOf(theme)
|
listOf(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentList = contentService.getLatestContentByTheme(
|
return contentService.getLatestContentByTheme(
|
||||||
theme = themeList,
|
theme = themeList,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
isFree = false,
|
isFree = false,
|
||||||
@@ -463,39 +261,6 @@ class HomeService(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* contentList 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 contentList title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedContentList = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val contentIds = contentList.map { it.contentId }
|
|
||||||
|
|
||||||
if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
contentList.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentList
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentList
|
|
||||||
}
|
|
||||||
|
|
||||||
return translatedContentList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDayOfWeekSeriesList(
|
fun getDayOfWeekSeriesList(
|
||||||
@@ -571,8 +336,7 @@ class HomeService(
|
|||||||
fun getRecommendContentList(
|
fun getRecommendContentList(
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
member: Member?,
|
member: Member?
|
||||||
languageCode: String? = null
|
|
||||||
): List<AudioContentMainItem> {
|
): List<AudioContentMainItem> {
|
||||||
val memberId = member?.id
|
val memberId = member?.id
|
||||||
val isAdult = member?.auth != null && isAdultContentVisible
|
val isAdult = member?.auth != null && isAdultContentVisible
|
||||||
@@ -607,37 +371,6 @@ class HomeService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return result
|
||||||
* 추천 콘텐츠 번역 데이터 조회
|
|
||||||
*
|
|
||||||
* languageCode != null
|
|
||||||
* contentTranslationRepository를 이용해 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
*
|
|
||||||
* 한 번에 조회하고 contentId를 매핑하여 result의 title을 번역 데이터로 변경한다
|
|
||||||
*/
|
|
||||||
val translatedResult = if (!languageCode.isNullOrBlank()) {
|
|
||||||
val contentIds = result.map { it.contentId }
|
|
||||||
|
|
||||||
if (contentIds.isNotEmpty()) {
|
|
||||||
val translations = contentTranslationRepository
|
|
||||||
.findByContentIdInAndLocale(contentIds = contentIds, locale = languageCode)
|
|
||||||
.associateBy { it.contentId }
|
|
||||||
|
|
||||||
result.map { item ->
|
|
||||||
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
|
|
||||||
if (translatedTitle.isNullOrBlank()) {
|
|
||||||
item
|
|
||||||
} else {
|
|
||||||
item.copy(title = translatedTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
return translatedResult
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ class ChatCharacter(
|
|||||||
// 캐릭터 한 줄 소개
|
// 캐릭터 한 줄 소개
|
||||||
var description: String,
|
var description: String,
|
||||||
|
|
||||||
var languageCode: String? = null,
|
|
||||||
|
|
||||||
// AI 시스템 프롬프트
|
// AI 시스템 프롬프트
|
||||||
@Column(columnDefinition = "TEXT", nullable = false)
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
var systemPrompt: String,
|
var systemPrompt: String,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ 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, request.languageCode)
|
val id = service.addReply(characterId, commentId, member, request.comment)
|
||||||
ApiResponse.ok(id)
|
ApiResponse.ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ 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
|
||||||
@@ -21,8 +20,7 @@ data class CharacterCommentResponse(
|
|||||||
val memberNickname: String,
|
val memberNickname: String,
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
val replyCount: Int,
|
val replyCount: Int,
|
||||||
val comment: String,
|
val comment: String
|
||||||
val languageCode: String?
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 답글 Response 단건(목록 원소)
|
// 답글 Response 단건(목록 원소)
|
||||||
@@ -37,8 +35,7 @@ data class CharacterReplyResponse(
|
|||||||
val memberProfileImage: String,
|
val memberProfileImage: String,
|
||||||
val memberNickname: String,
|
val memberNickname: String,
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
val comment: String,
|
val comment: String
|
||||||
val languageCode: String?
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 댓글의 답글 조회 Response 컨테이너
|
// 댓글의 답글 조회 Response 컨테이너
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ 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
|
||||||
@@ -15,8 +12,7 @@ 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 {
|
||||||
@@ -44,8 +40,7 @@ class CharacterCommentService(
|
|||||||
memberNickname = member.nickname,
|
memberNickname = member.nickname,
|
||||||
createdAt = toEpochMilli(entity.createdAt),
|
createdAt = toEpochMilli(entity.createdAt),
|
||||||
replyCount = replyCountOverride ?: commentRepository.countByParent_IdAndIsActiveTrue(entity.id!!),
|
replyCount = replyCountOverride ?: commentRepository.countByParent_IdAndIsActiveTrue(entity.id!!),
|
||||||
comment = entity.comment,
|
comment = entity.comment
|
||||||
languageCode = entity.languageCode
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,44 +52,25 @@ class CharacterCommentService(
|
|||||||
memberProfileImage = profileUrl(imageHost, member.profileImage),
|
memberProfileImage = profileUrl(imageHost, member.profileImage),
|
||||||
memberNickname = member.nickname,
|
memberNickname = member.nickname,
|
||||||
createdAt = toEpochMilli(entity.createdAt),
|
createdAt = toEpochMilli(entity.createdAt),
|
||||||
comment = entity.comment,
|
comment = entity.comment
|
||||||
languageCode = entity.languageCode
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun addComment(characterId: Long, member: Member, text: String, languageCode: String? = null): Long {
|
fun addComment(characterId: Long, member: Member, text: String): 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, languageCode = languageCode)
|
val entity = CharacterComment(comment = text)
|
||||||
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(
|
fun addReply(characterId: Long, parentCommentId: Long, member: Member, text: String): Long {
|
||||||
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("댓글을 찾을 수 없습니다.") }
|
||||||
@@ -102,23 +78,11 @@ 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, languageCode = languageCode)
|
val entity = CharacterComment(comment = text)
|
||||||
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!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.controller
|
package kr.co.vividnext.sodalive.chat.character.controller
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentService
|
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentService
|
||||||
import kr.co.vividnext.sodalive.chat.character.curation.CharacterCurationQueryService
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBackgroundResponse
|
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBackgroundResponse
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBannerResponse
|
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBannerResponse
|
||||||
@@ -13,17 +12,9 @@ import kr.co.vividnext.sodalive.chat.character.dto.OtherCharacter
|
|||||||
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
|
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslation
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRenderedPayload
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterBackground
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterDetail
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterPersonality
|
|
||||||
import kr.co.vividnext.sodalive.chat.room.service.ChatRoomService
|
import kr.co.vividnext.sodalive.chat.room.service.ChatRoomService
|
||||||
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.translation.PapagoTranslationService
|
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
|
||||||
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.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
@@ -41,10 +32,7 @@ class ChatCharacterController(
|
|||||||
private val bannerService: ChatCharacterBannerService,
|
private val bannerService: ChatCharacterBannerService,
|
||||||
private val chatRoomService: ChatRoomService,
|
private val chatRoomService: ChatRoomService,
|
||||||
private val characterCommentService: CharacterCommentService,
|
private val characterCommentService: CharacterCommentService,
|
||||||
private val curationQueryService: CharacterCurationQueryService,
|
private val curationQueryService: kr.co.vividnext.sodalive.chat.character.curation.CharacterCurationQueryService,
|
||||||
|
|
||||||
private val translationService: PapagoTranslationService,
|
|
||||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
|
||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
@@ -131,7 +119,6 @@ class ChatCharacterController(
|
|||||||
@GetMapping("/{characterId}")
|
@GetMapping("/{characterId}")
|
||||||
fun getCharacterDetail(
|
fun getCharacterDetail(
|
||||||
@PathVariable characterId: Long,
|
@PathVariable characterId: Long,
|
||||||
@RequestParam(required = false, defaultValue = "ko") languageCode: String? = "ko",
|
|
||||||
@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("로그인 정보를 확인해주세요.")
|
||||||
@@ -161,122 +148,6 @@ class ChatCharacterController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var translated: TranslatedAiCharacterDetail? = null
|
|
||||||
if (!languageCode.isNullOrBlank() && languageCode != character.languageCode) {
|
|
||||||
val locale = languageCode.lowercase()
|
|
||||||
|
|
||||||
val existing = aiCharacterTranslationRepository
|
|
||||||
.findByCharacterIdAndLocale(character.id!!, locale)
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
val payload = existing.renderedPayload
|
|
||||||
translated = TranslatedAiCharacterDetail(
|
|
||||||
name = payload.name,
|
|
||||||
description = payload.description,
|
|
||||||
gender = payload.gender,
|
|
||||||
personality = TranslatedAiCharacterPersonality(
|
|
||||||
trait = payload.personalityTrait,
|
|
||||||
description = payload.personalityDescription
|
|
||||||
).takeIf {
|
|
||||||
(it.trait?.isNotBlank() == true) || (it.description?.isNotBlank() == true)
|
|
||||||
},
|
|
||||||
background = TranslatedAiCharacterBackground(
|
|
||||||
topic = payload.backgroundTopic,
|
|
||||||
description = payload.backgroundDescription
|
|
||||||
).takeIf {
|
|
||||||
(it.topic?.isNotBlank() == true) || (it.description?.isNotBlank() == true)
|
|
||||||
},
|
|
||||||
tags = payload.tags
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val texts = mutableListOf<String>()
|
|
||||||
texts.add(character.name)
|
|
||||||
texts.add(character.description)
|
|
||||||
texts.add(character.gender ?: "")
|
|
||||||
|
|
||||||
val hasPersonality = personality != null
|
|
||||||
if (hasPersonality) {
|
|
||||||
texts.add(personality!!.trait)
|
|
||||||
texts.add(personality.description)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasBackground = background != null
|
|
||||||
if (hasBackground) {
|
|
||||||
texts.add(background!!.topic)
|
|
||||||
texts.add(background.description)
|
|
||||||
}
|
|
||||||
|
|
||||||
texts.add(tags)
|
|
||||||
|
|
||||||
val sourceLanguage = character.languageCode ?: "ko"
|
|
||||||
|
|
||||||
val response = translationService.translate(
|
|
||||||
request = TranslateRequest(
|
|
||||||
texts = texts,
|
|
||||||
sourceLanguage = sourceLanguage,
|
|
||||||
targetLanguage = locale
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val translatedTexts = response.translatedText
|
|
||||||
if (translatedTexts.size == texts.size) {
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
val translatedName = translatedTexts[index++]
|
|
||||||
val translatedDescription = translatedTexts[index++]
|
|
||||||
val translatedGender = translatedTexts[index++]
|
|
||||||
|
|
||||||
var translatedPersonality: TranslatedAiCharacterPersonality? = null
|
|
||||||
if (hasPersonality) {
|
|
||||||
translatedPersonality = TranslatedAiCharacterPersonality(
|
|
||||||
trait = translatedTexts[index++],
|
|
||||||
description = translatedTexts[index++]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var translatedBackground: TranslatedAiCharacterBackground? = null
|
|
||||||
if (hasBackground) {
|
|
||||||
translatedBackground = TranslatedAiCharacterBackground(
|
|
||||||
topic = translatedTexts[index++],
|
|
||||||
description = translatedTexts[index++]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val translatedTags = translatedTexts[index]
|
|
||||||
|
|
||||||
val payload = AiCharacterTranslationRenderedPayload(
|
|
||||||
name = translatedName,
|
|
||||||
description = translatedDescription,
|
|
||||||
gender = translatedGender,
|
|
||||||
personalityTrait = translatedPersonality?.trait ?: "",
|
|
||||||
personalityDescription = translatedPersonality?.description ?: "",
|
|
||||||
backgroundTopic = translatedBackground?.topic ?: "",
|
|
||||||
backgroundDescription = translatedBackground?.description ?: "",
|
|
||||||
tags = translatedTags
|
|
||||||
)
|
|
||||||
|
|
||||||
val entity = AiCharacterTranslation(
|
|
||||||
characterId = character.id!!,
|
|
||||||
locale = locale,
|
|
||||||
translatedName = translatedName,
|
|
||||||
translatedTags = translatedTags,
|
|
||||||
renderedPayload = payload
|
|
||||||
)
|
|
||||||
|
|
||||||
aiCharacterTranslationRepository.save(entity)
|
|
||||||
|
|
||||||
translated = TranslatedAiCharacterDetail(
|
|
||||||
name = translatedName,
|
|
||||||
description = translatedDescription,
|
|
||||||
gender = translatedGender,
|
|
||||||
personality = translatedPersonality,
|
|
||||||
background = translatedBackground,
|
|
||||||
tags = translatedTags
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다른 캐릭터 조회 (태그 기반, 랜덤 10개, 현재 캐릭터 제외)
|
// 다른 캐릭터 조회 (태그 기반, 랜덤 10개, 현재 캐릭터 제외)
|
||||||
val others = service.getOtherCharactersBySharedTags(characterId, 10)
|
val others = service.getOtherCharactersBySharedTags(characterId, 10)
|
||||||
.map { other ->
|
.map { other ->
|
||||||
@@ -300,7 +171,6 @@ class ChatCharacterController(
|
|||||||
characterId = character.id!!,
|
characterId = character.id!!,
|
||||||
name = character.name,
|
name = character.name,
|
||||||
description = character.description,
|
description = character.description,
|
||||||
languageCode = character.languageCode,
|
|
||||||
mbti = character.mbti,
|
mbti = character.mbti,
|
||||||
gender = character.gender,
|
gender = character.gender,
|
||||||
age = character.age,
|
age = character.age,
|
||||||
@@ -313,8 +183,7 @@ class ChatCharacterController(
|
|||||||
characterType = character.characterType,
|
characterType = character.characterType,
|
||||||
others = others,
|
others = others,
|
||||||
latestComment = latestComment,
|
latestComment = latestComment,
|
||||||
totalComments = characterCommentService.getTotalCommentCount(character.id!!),
|
totalComments = characterCommentService.getTotalCommentCount(character.id!!)
|
||||||
translated = translated
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ package kr.co.vividnext.sodalive.chat.character.dto
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.CharacterType
|
import kr.co.vividnext.sodalive.chat.character.CharacterType
|
||||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentResponse
|
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentResponse
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterDetail
|
|
||||||
|
|
||||||
data class CharacterDetailResponse(
|
data class CharacterDetailResponse(
|
||||||
val characterId: Long,
|
val characterId: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val languageCode: String?,
|
|
||||||
val mbti: String?,
|
val mbti: String?,
|
||||||
val gender: String?,
|
val gender: String?,
|
||||||
val age: Int?,
|
val age: Int?,
|
||||||
@@ -21,8 +19,7 @@ data class CharacterDetailResponse(
|
|||||||
val characterType: CharacterType,
|
val characterType: CharacterType,
|
||||||
val others: List<OtherCharacter>,
|
val others: List<OtherCharacter>,
|
||||||
val latestComment: CharacterCommentResponse?,
|
val latestComment: CharacterCommentResponse?,
|
||||||
val totalComments: Int,
|
val totalComments: Int
|
||||||
val translated: TranslatedAiCharacterDetail?
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OtherCharacter(
|
data class OtherCharacter(
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.translate
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
|
||||||
import javax.persistence.AttributeConverter
|
|
||||||
import javax.persistence.Column
|
|
||||||
import javax.persistence.Convert
|
|
||||||
import javax.persistence.Converter
|
|
||||||
import javax.persistence.Entity
|
|
||||||
import javax.persistence.Table
|
|
||||||
import javax.persistence.UniqueConstraint
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(
|
|
||||||
uniqueConstraints = [
|
|
||||||
UniqueConstraint(columnNames = ["characterId", "locale"])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
class AiCharacterTranslation(
|
|
||||||
val characterId: Long,
|
|
||||||
val locale: String,
|
|
||||||
val translatedName: String,
|
|
||||||
val translatedTags: String,
|
|
||||||
|
|
||||||
@Column(columnDefinition = "json")
|
|
||||||
@Convert(converter = AiCharacterTranslationRenderedPayloadConverter::class)
|
|
||||||
val renderedPayload: AiCharacterTranslationRenderedPayload
|
|
||||||
) : BaseEntity()
|
|
||||||
|
|
||||||
data class AiCharacterTranslationRenderedPayload(
|
|
||||||
val name: String,
|
|
||||||
val description: String,
|
|
||||||
val gender: String,
|
|
||||||
val personalityTrait: String,
|
|
||||||
val personalityDescription: String,
|
|
||||||
val backgroundTopic: String,
|
|
||||||
val backgroundDescription: String,
|
|
||||||
val tags: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Converter(autoApply = false)
|
|
||||||
class AiCharacterTranslationRenderedPayloadConverter :
|
|
||||||
AttributeConverter<AiCharacterTranslationRenderedPayload, String> {
|
|
||||||
|
|
||||||
override fun convertToDatabaseColumn(attribute: AiCharacterTranslationRenderedPayload?): String {
|
|
||||||
if (attribute == null) return "{}"
|
|
||||||
return objectMapper.writeValueAsString(attribute)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convertToEntityAttribute(dbData: String?): AiCharacterTranslationRenderedPayload {
|
|
||||||
if (dbData.isNullOrBlank()) {
|
|
||||||
return AiCharacterTranslationRenderedPayload(
|
|
||||||
name = "",
|
|
||||||
description = "",
|
|
||||||
gender = "",
|
|
||||||
personalityTrait = "",
|
|
||||||
personalityDescription = "",
|
|
||||||
backgroundTopic = "",
|
|
||||||
backgroundDescription = "",
|
|
||||||
tags = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return objectMapper.readValue(dbData)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val objectMapper = jacksonObjectMapper()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class TranslatedAiCharacterDetail(
|
|
||||||
val name: String?,
|
|
||||||
val description: String?,
|
|
||||||
val gender: String?,
|
|
||||||
val personality: TranslatedAiCharacterPersonality?,
|
|
||||||
val background: TranslatedAiCharacterBackground?,
|
|
||||||
val tags: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TranslatedAiCharacterPersonality(
|
|
||||||
val trait: String?,
|
|
||||||
val description: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TranslatedAiCharacterBackground(
|
|
||||||
val topic: String?,
|
|
||||||
val description: String?
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.translate
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
|
|
||||||
interface AiCharacterTranslationRepository : JpaRepository<AiCharacterTranslation, Long> {
|
|
||||||
fun findByCharacterIdAndLocale(characterId: Long, locale: String): AiCharacterTranslation?
|
|
||||||
|
|
||||||
fun findByCharacterIdInAndLocale(characterIds: List<Long>, locale: String): List<AiCharacterTranslation>
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,6 @@ 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,
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ class AudioContentController(private val service: AudioContentService) {
|
|||||||
fun getDetail(
|
fun getDetail(
|
||||||
@PathVariable id: Long,
|
@PathVariable id: Long,
|
||||||
@RequestParam timezone: String,
|
@RequestParam timezone: String,
|
||||||
@RequestParam(required = false) languageCode: String? = null,
|
|
||||||
@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 {
|
||||||
@@ -142,8 +141,7 @@ class AudioContentController(private val service: AudioContentService) {
|
|||||||
id = id,
|
id = id,
|
||||||
member = member,
|
member = member,
|
||||||
isAdultContentVisible = isAdultContentVisible ?: true,
|
isAdultContentVisible = isAdultContentVisible ?: true,
|
||||||
timezone = timezone,
|
timezone = timezone
|
||||||
languageCode = languageCode
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,10 @@ import kr.co.vividnext.sodalive.content.order.OrderType
|
|||||||
import kr.co.vividnext.sodalive.content.pin.PinContent
|
import kr.co.vividnext.sodalive.content.pin.PinContent
|
||||||
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
|
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
|
||||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
|
||||||
import kr.co.vividnext.sodalive.content.translation.TranslatedContent
|
|
||||||
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
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.translation.PapagoTranslationService
|
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
|
||||||
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 kr.co.vividnext.sodalive.utils.generateFileName
|
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||||
@@ -60,9 +56,6 @@ class AudioContentService(
|
|||||||
private val audioContentLikeRepository: AudioContentLikeRepository,
|
private val audioContentLikeRepository: AudioContentLikeRepository,
|
||||||
private val pinContentRepository: PinContentRepository,
|
private val pinContentRepository: PinContentRepository,
|
||||||
|
|
||||||
private val translationService: PapagoTranslationService,
|
|
||||||
private val contentTranslationRepository: ContentTranslationRepository,
|
|
||||||
|
|
||||||
private val s3Uploader: S3Uploader,
|
private val s3Uploader: S3Uploader,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val audioContentCloudFront: AudioContentCloudFront,
|
private val audioContentCloudFront: AudioContentCloudFront,
|
||||||
@@ -245,7 +238,6 @@ 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 {
|
||||||
@@ -339,24 +331,6 @@ 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!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,13 +477,11 @@ class AudioContentService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
fun getDetail(
|
fun getDetail(
|
||||||
id: Long,
|
id: Long,
|
||||||
member: Member,
|
member: Member,
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
timezone: String,
|
timezone: String
|
||||||
languageCode: String?
|
|
||||||
): GetAudioContentDetailResponse {
|
): GetAudioContentDetailResponse {
|
||||||
val isAdult = member.auth != null && isAdultContentVisible
|
val isAdult = member.auth != null && isAdultContentVisible
|
||||||
|
|
||||||
@@ -727,93 +699,10 @@ class AudioContentService(
|
|||||||
listOf()
|
listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
var translated: TranslatedContent? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* audioContent.languageCode != languageCode
|
|
||||||
*
|
|
||||||
* 번역 콘텐츠를 조회한다. - contentId, locale
|
|
||||||
* 번역 콘텐츠가 있으면
|
|
||||||
* TranslatedContent로 가공한다
|
|
||||||
*
|
|
||||||
* 번역 콘텐츠가 없으면
|
|
||||||
* 파파고 API를 통해 번역한 후 저장한다.
|
|
||||||
*
|
|
||||||
* 번역 대상: title, detail, tags
|
|
||||||
*
|
|
||||||
* 파파고로 번역한 데이터를 TranslatedContent로 가공한다
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
audioContent.languageCode != null &&
|
|
||||||
audioContent.languageCode!!.isNotBlank() &&
|
|
||||||
!languageCode.isNullOrBlank() &&
|
|
||||||
audioContent.languageCode != languageCode
|
|
||||||
) {
|
|
||||||
val locale = languageCode.lowercase()
|
|
||||||
|
|
||||||
val existing = contentTranslationRepository
|
|
||||||
.findByContentIdAndLocale(audioContent.id!!, locale)
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
val payload = existing.renderedPayload
|
|
||||||
translated = TranslatedContent(
|
|
||||||
title = payload.title,
|
|
||||||
detail = payload.detail,
|
|
||||||
tags = payload.tags
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val texts = mutableListOf<String>()
|
|
||||||
texts.add(audioContent.title)
|
|
||||||
texts.add(audioContent.detail)
|
|
||||||
texts.add(tag)
|
|
||||||
|
|
||||||
val sourceLanguage = audioContent.languageCode ?: "ko"
|
|
||||||
|
|
||||||
val response = translationService.translate(
|
|
||||||
request = TranslateRequest(
|
|
||||||
texts = texts,
|
|
||||||
sourceLanguage = sourceLanguage,
|
|
||||||
targetLanguage = locale
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val translatedTexts = response.translatedText
|
|
||||||
if (translatedTexts.size == texts.size) {
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
val translatedTitle = translatedTexts[index++]
|
|
||||||
val translatedDetail = translatedTexts[index++]
|
|
||||||
val translatedTags = translatedTexts[index]
|
|
||||||
|
|
||||||
val payload = kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload(
|
|
||||||
title = translatedTitle,
|
|
||||||
detail = translatedDetail,
|
|
||||||
tags = translatedTags
|
|
||||||
)
|
|
||||||
|
|
||||||
contentTranslationRepository.save(
|
|
||||||
kr.co.vividnext.sodalive.content.translation.ContentTranslation(
|
|
||||||
contentId = audioContent.id!!,
|
|
||||||
locale = locale,
|
|
||||||
translatedTitle = translatedTitle,
|
|
||||||
renderedPayload = payload
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
translated = TranslatedContent(
|
|
||||||
title = translatedTitle,
|
|
||||||
detail = translatedDetail,
|
|
||||||
tags = translatedTags
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetAudioContentDetailResponse(
|
return GetAudioContentDetailResponse(
|
||||||
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,
|
||||||
@@ -856,8 +745,7 @@ class AudioContentService(
|
|||||||
previousContent = previousContent,
|
previousContent = previousContent,
|
||||||
nextContent = nextContent,
|
nextContent = nextContent,
|
||||||
buyerList = buyerList,
|
buyerList = buyerList,
|
||||||
isAvailableUsePoint = audioContent.isPointAvailable,
|
isAvailableUsePoint = audioContent.isPointAvailable
|
||||||
translated = translated
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,5 @@ 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
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ package kr.co.vividnext.sodalive.content
|
|||||||
import com.querydsl.core.annotations.QueryProjection
|
import com.querydsl.core.annotations.QueryProjection
|
||||||
import kr.co.vividnext.sodalive.content.comment.GetAudioContentCommentListItem
|
import kr.co.vividnext.sodalive.content.comment.GetAudioContentCommentListItem
|
||||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||||
import kr.co.vividnext.sodalive.content.translation.TranslatedContent
|
|
||||||
|
|
||||||
data class GetAudioContentDetailResponse(
|
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,
|
||||||
@@ -41,8 +39,7 @@ data class GetAudioContentDetailResponse(
|
|||||||
val previousContent: OtherContentResponse?,
|
val previousContent: OtherContentResponse?,
|
||||||
val nextContent: OtherContentResponse?,
|
val nextContent: OtherContentResponse?,
|
||||||
val buyerList: List<ContentBuyer>,
|
val buyerList: List<ContentBuyer>,
|
||||||
val isAvailableUsePoint: Boolean,
|
val isAvailableUsePoint: Boolean
|
||||||
val translated: TranslatedContent?
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OtherContentResponse @QueryProjection constructor(
|
data class OtherContentResponse @QueryProjection constructor(
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.content
|
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
|
||||||
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,
|
|
||||||
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 chatCharacterRepository: ChatCharacterRepository,
|
|
||||||
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 -> handleCharacterLanguageDetect(event)
|
|
||||||
LanguageDetectTargetType.CHARACTER_COMMENT -> handleCharacterCommentLanguageDetect(event)
|
|
||||||
LanguageDetectTargetType.CREATOR_CHEERS -> handleCreatorCheersLanguageDetect(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleCharacterLanguageDetect(event: LanguageDetectEvent) {
|
|
||||||
val characterId = event.id
|
|
||||||
|
|
||||||
val character = chatCharacterRepository.findById(characterId).orElse(null)
|
|
||||||
if (character == null) {
|
|
||||||
log.warn("[PapagoLanguageDetect] ChatCharacter not found. characterId={}", characterId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
|
||||||
if (!character.languageCode.isNullOrBlank()) {
|
|
||||||
log.debug(
|
|
||||||
"[PapagoLanguageDetect] languageCode already set. Skip language detection. characterId={}, languageCode={}",
|
|
||||||
characterId,
|
|
||||||
character.languageCode
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val langCode = requestPapagoLanguageCode(event.query, characterId) ?: return
|
|
||||||
|
|
||||||
character.languageCode = langCode
|
|
||||||
chatCharacterRepository.save(character)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"[PapagoLanguageDetect] languageCode updated from Papago. characterId={}, langCode={}",
|
|
||||||
characterId,
|
|
||||||
langCode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,7 +16,6 @@ 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,8 +32,7 @@ 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 {
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
audioContentComment.member.nickname,
|
audioContentComment.member.nickname,
|
||||||
audioContentComment.member.profileImage.prepend("/").prepend(cloudFrontHost),
|
audioContentComment.member.profileImage.prepend("/").prepend(cloudFrontHost),
|
||||||
audioContentComment.comment,
|
audioContentComment.comment,
|
||||||
audioContentComment.languageCode,
|
|
||||||
audioContentComment.isSecret,
|
audioContentComment.isSecret,
|
||||||
audioContentComment.donationCan.coalesce(0),
|
audioContentComment.donationCan.coalesce(0),
|
||||||
formattedDate,
|
formattedDate,
|
||||||
@@ -167,7 +166,6 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
audioContentComment.member.nickname,
|
audioContentComment.member.nickname,
|
||||||
audioContentComment.member.profileImage.prepend("/").prepend(cloudFrontHost),
|
audioContentComment.member.profileImage.prepend("/").prepend(cloudFrontHost),
|
||||||
audioContentComment.comment,
|
audioContentComment.comment,
|
||||||
audioContentComment.languageCode,
|
|
||||||
audioContentComment.isSecret,
|
audioContentComment.isSecret,
|
||||||
audioContentComment.donationCan.coalesce(0),
|
audioContentComment.donationCan.coalesce(0),
|
||||||
formattedDate,
|
formattedDate,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ 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
|
||||||
@@ -34,8 +32,7 @@ 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다시 시도해 주세요.")
|
||||||
@@ -53,7 +50,7 @@ class AudioContentCommentService(
|
|||||||
throw SodaException("콘텐츠 구매 후 비밀댓글을 등록할 수 있습니다.")
|
throw SodaException("콘텐츠 구매 후 비밀댓글을 등록할 수 있습니다.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val audioContentComment = AudioContentComment(comment = comment, languageCode = languageCode, isSecret = isSecret)
|
val audioContentComment = AudioContentComment(comment = comment, isSecret = isSecret)
|
||||||
audioContentComment.audioContent = audioContent
|
audioContentComment.audioContent = audioContent
|
||||||
audioContentComment.member = member
|
audioContentComment.member = member
|
||||||
|
|
||||||
@@ -88,17 +85,6 @@ class AudioContentCommentService(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 언어 코드가 지정되지 않은 경우, 파파고 언어 감지 API를 통해 비동기로 언어를 식별한다.
|
|
||||||
if (languageCode.isNullOrBlank()) {
|
|
||||||
applicationEventPublisher.publishEvent(
|
|
||||||
LanguageDetectEvent(
|
|
||||||
id = savedContentComment.id!!,
|
|
||||||
query = comment,
|
|
||||||
targetType = LanguageDetectTargetType.COMMENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return savedContentComment.id!!
|
return savedContentComment.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ data class GetAudioContentCommentListItem @QueryProjection constructor(
|
|||||||
val nickname: String,
|
val nickname: String,
|
||||||
val profileUrl: String,
|
val profileUrl: String,
|
||||||
val comment: String,
|
val comment: String,
|
||||||
val languageCode: String?,
|
|
||||||
val isSecret: Boolean,
|
val isSecret: Boolean,
|
||||||
val donationCan: Int,
|
val donationCan: Int,
|
||||||
val date: String,
|
val date: String,
|
||||||
|
|||||||
@@ -4,6 +4,5 @@ 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,6 +4,5 @@ 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,12 +4,9 @@ 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
|
||||||
|
|
||||||
@@ -17,8 +14,7 @@ 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) {
|
||||||
@@ -38,23 +34,10 @@ 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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.content.translation
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
|
||||||
import javax.persistence.AttributeConverter
|
|
||||||
import javax.persistence.Column
|
|
||||||
import javax.persistence.Convert
|
|
||||||
import javax.persistence.Converter
|
|
||||||
import javax.persistence.Entity
|
|
||||||
import javax.persistence.Table
|
|
||||||
import javax.persistence.UniqueConstraint
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(
|
|
||||||
uniqueConstraints = [
|
|
||||||
UniqueConstraint(columnNames = ["contentId", "locale"])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
class ContentTranslation(
|
|
||||||
val contentId: Long,
|
|
||||||
val locale: String,
|
|
||||||
val translatedTitle: String,
|
|
||||||
|
|
||||||
@Column(columnDefinition = "json")
|
|
||||||
@Convert(converter = ContentTranslationPayloadConverter::class)
|
|
||||||
val renderedPayload: ContentTranslationPayload
|
|
||||||
) : BaseEntity()
|
|
||||||
|
|
||||||
data class ContentTranslationPayload(
|
|
||||||
val title: String,
|
|
||||||
val detail: String,
|
|
||||||
val tags: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Converter(autoApply = false)
|
|
||||||
class ContentTranslationPayloadConverter : AttributeConverter<ContentTranslationPayload, String> {
|
|
||||||
|
|
||||||
override fun convertToDatabaseColumn(attribute: ContentTranslationPayload?): String {
|
|
||||||
if (attribute == null) return "{}"
|
|
||||||
return objectMapper.writeValueAsString(attribute)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convertToEntityAttribute(dbData: String?): ContentTranslationPayload {
|
|
||||||
if (dbData.isNullOrBlank()) {
|
|
||||||
return ContentTranslationPayload(
|
|
||||||
title = "",
|
|
||||||
detail = "",
|
|
||||||
tags = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return objectMapper.readValue(dbData)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val objectMapper = jacksonObjectMapper()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class TranslatedContent(
|
|
||||||
val title: String?,
|
|
||||||
val detail: String?,
|
|
||||||
val tags: String?
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.content.translation
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
|
|
||||||
interface ContentTranslationRepository : JpaRepository<ContentTranslation, Long> {
|
|
||||||
fun findByContentIdAndLocale(contentId: Long, locale: String): ContentTranslation?
|
|
||||||
|
|
||||||
fun findByContentIdInAndLocale(contentIds: List<Long>, locale: String): List<ContentTranslation>
|
|
||||||
}
|
|
||||||
@@ -488,7 +488,6 @@ class ExplorerQueryRepository(
|
|||||||
"$cloudFrontHost/profile/default-profile.png"
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
},
|
},
|
||||||
content = it.cheers,
|
content = it.cheers,
|
||||||
languageCode = it.languageCode,
|
|
||||||
date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")),
|
date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")),
|
||||||
replyList = it.children.asSequence()
|
replyList = it.children.asSequence()
|
||||||
.map { cheers ->
|
.map { cheers ->
|
||||||
@@ -506,7 +505,6 @@ class ExplorerQueryRepository(
|
|||||||
"$cloudFrontHost/profile/default-profile.png"
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
},
|
},
|
||||||
content = cheers.cheers,
|
content = cheers.cheers,
|
||||||
languageCode = cheers.languageCode,
|
|
||||||
date = replyDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")),
|
date = replyDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")),
|
||||||
replyList = listOf()
|
replyList = listOf()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ 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
|
||||||
@@ -443,7 +441,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, languageCode = request.languageCode)
|
val cheers = CreatorCheers(cheers = request.content)
|
||||||
cheers.member = member
|
cheers.member = member
|
||||||
cheers.creator = creator
|
cheers.creator = creator
|
||||||
|
|
||||||
@@ -458,17 +456,6 @@ 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(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ data class GetCheersResponseItem(
|
|||||||
val nickname: String,
|
val nickname: String,
|
||||||
val profileUrl: String,
|
val profileUrl: String,
|
||||||
val content: String,
|
val content: String,
|
||||||
val languageCode: String?,
|
|
||||||
val date: String,
|
val date: String,
|
||||||
val replyList: List<GetCheersResponseItem>
|
val replyList: List<GetCheersResponseItem>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ 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,6 +3,5 @@ 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
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Papago 번역 API 응답 예시
|
|
||||||
*
|
|
||||||
* ```json
|
|
||||||
* {
|
|
||||||
* "message": {
|
|
||||||
* "result": {
|
|
||||||
* "srcLangType": "ko",
|
|
||||||
* "tarLangType": "en",
|
|
||||||
* "translatedText": "Hello, I like to eat apple while riding a bicycle."
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 위 JSON 구조에 대응하는 최상위 응답 모델
|
|
||||||
*/
|
|
||||||
data class PapagoTranslationResponse(
|
|
||||||
val message: Message
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* message 필드 내부 구조
|
|
||||||
*/
|
|
||||||
data class Message(
|
|
||||||
val result: Result
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 실제 번역 결과 데이터
|
|
||||||
*/
|
|
||||||
data class Result(
|
|
||||||
val srcLangType: String,
|
|
||||||
val tarLangType: String,
|
|
||||||
val translatedText: String
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
|
||||||
|
|
||||||
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.stereotype.Service
|
|
||||||
import org.springframework.web.client.RestTemplate
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class PapagoTranslationService(
|
|
||||||
@Value("\${cloud.naver.papago-client-id}")
|
|
||||||
private val papagoClientId: String,
|
|
||||||
|
|
||||||
@Value("\${cloud.naver.papago-client-secret}")
|
|
||||||
private val papagoClientSecret: String
|
|
||||||
) {
|
|
||||||
private val restTemplate: RestTemplate = RestTemplate()
|
|
||||||
|
|
||||||
private val papagoTranslateUrl = "https://papago.apigw.ntruss.com/nmt/v1/translation"
|
|
||||||
|
|
||||||
fun translate(request: TranslateRequest): TranslateResult {
|
|
||||||
if (request.texts.isEmpty() || request.sourceLanguage == request.targetLanguage) {
|
|
||||||
return TranslateResult(emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
validateLanguages(request.sourceLanguage, request.targetLanguage)
|
|
||||||
|
|
||||||
val headers = HttpHeaders().apply {
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
set("X-NCP-APIGW-API-KEY-ID", papagoClientId)
|
|
||||||
set("X-NCP-APIGW-API-KEY", papagoClientSecret)
|
|
||||||
}
|
|
||||||
|
|
||||||
val translatedTexts = mutableListOf<String>()
|
|
||||||
|
|
||||||
request.texts.forEach { text ->
|
|
||||||
try {
|
|
||||||
val body = mapOf(
|
|
||||||
"source" to request.sourceLanguage,
|
|
||||||
"target" to request.targetLanguage,
|
|
||||||
"text" to text
|
|
||||||
)
|
|
||||||
|
|
||||||
val requestEntity = HttpEntity(body, headers)
|
|
||||||
|
|
||||||
val response = restTemplate.postForEntity(
|
|
||||||
papagoTranslateUrl,
|
|
||||||
requestEntity,
|
|
||||||
PapagoTranslationResponse::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.statusCode.is2xxSuccessful) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val translated = response.body?.message?.result?.translatedText
|
|
||||||
translatedTexts.add(translated ?: "")
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return TranslateResult(translatedTexts)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateLanguages(sourceLanguage: String, targetLanguage: String) {
|
|
||||||
requireSupportedLanguage(sourceLanguage)
|
|
||||||
requireSupportedLanguage(targetLanguage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requireSupportedLanguage(language: String) {
|
|
||||||
val normalized = language.lowercase()
|
|
||||||
if (!SUPPORTED_LANGUAGE_CODES.contains(normalized)) {
|
|
||||||
throw IllegalArgumentException("지원하지 않는 언어 코드입니다: $language")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val SUPPORTED_LANGUAGE_CODES = setOf(
|
|
||||||
"ko",
|
|
||||||
"en",
|
|
||||||
"ja",
|
|
||||||
"zh-cn",
|
|
||||||
"zh-tw",
|
|
||||||
"es",
|
|
||||||
"fr",
|
|
||||||
"vi",
|
|
||||||
"th",
|
|
||||||
"id",
|
|
||||||
"de",
|
|
||||||
"ru",
|
|
||||||
"pt",
|
|
||||||
"it"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
|
||||||
|
|
||||||
data class TranslateRequest(
|
|
||||||
val texts: List<String>,
|
|
||||||
val sourceLanguage: String,
|
|
||||||
val targetLanguage: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TranslateResult(
|
|
||||||
val translatedText: List<String>
|
|
||||||
)
|
|
||||||
@@ -45,9 +45,6 @@ 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}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Check if a commit message follows project rules
|
|
||||||
# Rules: 50/72 formatting, no advertisements/branding
|
|
||||||
# Usage: ./check-commit-message-rules.sh [commit-hash]
|
|
||||||
# If no commit-hash is provided, checks the latest commit
|
|
||||||
|
|
||||||
# Determine which commit to check
|
|
||||||
if [ $# -eq 0 ]; then
|
|
||||||
commit_ref="HEAD"
|
|
||||||
echo "Checking latest commit..."
|
|
||||||
else
|
|
||||||
commit_ref="$1"
|
|
||||||
echo "Checking commit: $commit_ref"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get the commit message
|
|
||||||
commit_message=$(git log -1 --pretty=format:"%s%n%b" "$commit_ref")
|
|
||||||
|
|
||||||
# Split into subject and body
|
|
||||||
subject=$(echo "$commit_message" | head -n1)
|
|
||||||
body=$(echo "$commit_message" | tail -n +2 | sed '/^$/d')
|
|
||||||
|
|
||||||
echo "Checking commit message format..."
|
|
||||||
echo "Subject: $subject"
|
|
||||||
|
|
||||||
# Check subject line length
|
|
||||||
subject_length=${#subject}
|
|
||||||
if [ $subject_length -gt 50 ]; then
|
|
||||||
echo "[FAIL] Subject line too long: $subject_length characters (max 50)"
|
|
||||||
exit_code=1
|
|
||||||
else
|
|
||||||
echo "[PASS] Subject line length OK: $subject_length characters"
|
|
||||||
exit_code=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check body line lengths if body exists
|
|
||||||
if [ -n "$body" ]; then
|
|
||||||
echo "Checking body line lengths..."
|
|
||||||
while IFS= read -r line; do
|
|
||||||
line_length=${#line}
|
|
||||||
if [ $line_length -gt 72 ]; then
|
|
||||||
echo "[FAIL] Body line too long: $line_length characters (max 72)"
|
|
||||||
echo "Line: $line"
|
|
||||||
exit_code=1
|
|
||||||
fi
|
|
||||||
done <<< "$body"
|
|
||||||
|
|
||||||
if [ $exit_code -eq 0 ]; then
|
|
||||||
echo "[PASS] All body lines within 72 characters"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[INFO] No body content to check"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for advertisements, branding, or promotional content
|
|
||||||
echo "Checking for advertisements and branding..."
|
|
||||||
if echo "$commit_message" | grep -qi "generated with\|claude code\|anthropic\|co-authored-by.*claude\|🤖"; then
|
|
||||||
echo "[FAIL] Commit message contains advertisements, branding, or promotional content"
|
|
||||||
exit_code=1
|
|
||||||
else
|
|
||||||
echo "[PASS] No advertisements or branding detected"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $exit_code -eq 0 ]; then
|
|
||||||
echo "[PASS] Commit message follows all rules"
|
|
||||||
else
|
|
||||||
echo "[FAIL] Commit message violates project rules"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $exit_code
|
|
||||||
Reference in New Issue
Block a user