Compare commits
5 Commits
503802bcce
...
8636a8cac0
| Author | SHA1 | Date | |
|---|---|---|---|
| 8636a8cac0 | |||
| 304c001a27 | |||
| fdac55ebdf | |||
| 668d4f28cd | |||
| 7b0644cb66 |
@@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
@@ -12,9 +13,17 @@ 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
|
||||||
@@ -32,7 +41,10 @@ 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: kr.co.vividnext.sodalive.chat.character.curation.CharacterCurationQueryService,
|
private val curationQueryService: 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
|
||||||
@@ -119,6 +131,7 @@ 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("로그인 정보를 확인해주세요.")
|
||||||
@@ -148,6 +161,122 @@ 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 ->
|
||||||
@@ -184,7 +313,8 @@ 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,6 +2,7 @@ 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,
|
||||||
@@ -20,7 +21,8 @@ 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(
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
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 = ["character_id", "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?
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
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?
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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()) {
|
||||||
|
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 chunks = chunkTexts(request.texts)
|
||||||
|
val translatedTexts = mutableListOf<String>()
|
||||||
|
|
||||||
|
chunks.forEach { chunk ->
|
||||||
|
val textsInChunkCount = chunk.split(S3P_DELIMITER).size
|
||||||
|
|
||||||
|
try {
|
||||||
|
val body = mapOf(
|
||||||
|
"source" to request.sourceLanguage,
|
||||||
|
"target" to request.targetLanguage,
|
||||||
|
"text" to chunk
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
if (translated.isNullOrBlank()) {
|
||||||
|
repeat(textsInChunkCount) { translatedTexts.add("") }
|
||||||
|
} else {
|
||||||
|
translated.split(S3P_DELIMITER).forEach { translatedTexts.add(it) }
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
repeat(textsInChunkCount) { translatedTexts.add("") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chunkTexts(texts: List<String>): List<String> {
|
||||||
|
if (texts.isEmpty()) return emptyList()
|
||||||
|
val chunks = mutableListOf<String>()
|
||||||
|
var startIndex = 0
|
||||||
|
while (startIndex < texts.size) {
|
||||||
|
var endIndex = texts.size
|
||||||
|
while (endIndex > startIndex) {
|
||||||
|
val candidate = texts.subList(startIndex, endIndex)
|
||||||
|
val joined = candidate.joinToString(S3P_DELIMITER)
|
||||||
|
if (joined.length <= MAX_TEXT_LENGTH || endIndex - startIndex == 1) {
|
||||||
|
chunks.add(joined)
|
||||||
|
startIndex = endIndex
|
||||||
|
break
|
||||||
|
}
|
||||||
|
endIndex--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SUPPORTED_LANGUAGE_CODES = setOf(
|
||||||
|
"ko",
|
||||||
|
"en",
|
||||||
|
"ja",
|
||||||
|
"zh-cn",
|
||||||
|
"zh-tw",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"vi",
|
||||||
|
"th",
|
||||||
|
"id",
|
||||||
|
"de",
|
||||||
|
"ru",
|
||||||
|
"pt",
|
||||||
|
"it"
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val S3P_DELIMITER = "__S3P__"
|
||||||
|
private const val MAX_TEXT_LENGTH = 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
71
work/scripts/check-commit-message-rules.sh
Executable file
71
work/scripts/check-commit-message-rules.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/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