diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8a8b922 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +> 이 문서는 본 저장소에서 **AI Coding Agent가 반드시 따라야 할 개발 헌법(운영 규칙)**이다. +> 모든 신규 코드는 본 문서를 최우선 기준으로 작성한다. + +--- + +## 0. 전제 +질문에 대한 답변과 설명은 한국어로 한다. + +--- + +## 15. Commit Standards + +1. 커밋 메시지는 **반드시 한국어로 작성한다.** +2. 제목(subject)은 **현재형**으로 작성한다. (예: “기능 추가”, “기능 추가함” 금지) +3. 제목은 **50자 이내**로 작성한다. +4. 제목과 본문 사이에는 **반드시 한 줄 공백**을 둔다. +5. 본문은 **한 줄당 72자 이내**로 작성한다. +6. 하나의 문단에서는 72자를 초과할 때만 줄바꿈한다. +7. **공개 API 변경 사항만 설명**하며, 패키지 프라이빗 구현 상세는 포함하지 않는다. +8. **테스트 코드에 대한 언급은 커밋 메시지에 포함하지 않는다.** +9. 제목에 `fix:`, `feat:`, `docs:` 등의 **접두어를 사용하지 않는다.** +10. 제목은 **첫 글자를 대문자로 시작**한다. 단, 함수명 등 소문자가 합당한 경우만 예외를 허용한다. +11. 도구 광고, 브랜딩, 홍보성 문구를 **절대 포함하지 않는다.** +12. 커밋 전에는 **반드시 파일을 개별 stage 한다.** +13. 커밋 전 **`work/scripts/check-commit-message-rules.sh` 검증을 통과해야 한다.** + +--- + +## 16. AI 사용 규칙 (AI Interaction Rules) + +- 매우 작은 단위의 변경만 수행한다. +- 대규모 리팩터링은 반드시 사전 승인을 요청한다. + +--- + +✅ 본 문서는 **AI와 사람이 동일한 개발 규칙을 공유하기 위한 최상위 기준 문서**이며, +✅ 모든 신규 코드는 본 문서를 기준으로 검토된다. diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index d3116a3..27a7131 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -920,12 +920,34 @@ class AudioContentService( contentId = audioContent.id!! ) + /** + * themeStr 번역 처리 + */ + val themeStrTranslated = run { + val theme = audioContent.theme + if (theme?.id != null) { + val locale = langContext.lang.code + val translated = contentThemeTranslationRepository + .findByContentThemeIdAndLocale(theme.id!!, locale) + val text = translated?.theme + if (!text.isNullOrBlank()) text else theme.theme + } else { + audioContent.theme!!.theme + } + } + return GetAudioContentListItem( contentId = audioContent.id!!, coverImageUrl = "$coverImageHost/${audioContent.coverImage}", - title = audioContent.title, + title = run { + val translatedTitle = contentTranslationRepository + .findByContentIdAndLocale(audioContent.id!!, langContext.lang.code) + ?.renderedPayload + ?.title + if (translatedTitle.isNullOrBlank()) audioContent.title else translatedTitle + }, price = audioContent.price, - themeStr = audioContent.theme!!.theme, + themeStr = themeStrTranslated, duration = audioContent.duration, likeCount = likeCount, commentCount = commentCount, @@ -1017,9 +1039,42 @@ class AudioContentService( items } + // theme 번역 적용: 번역 데이터가 있으면 번역, 없으면 원문 유지 + val themeTranslatedList = run { + if (translatedContentList.isEmpty()) { + translatedContentList + } else { + val locale = langContext.lang.code + + // 활성 테마 목록에서 한글 원문 -> ID 매핑 구성 + val themesWithIds = themeQueryRepository.getActiveThemeWithIdsOfContent( + isAdult = isAdult, + isFree = false, + isPointAvailableOnly = false, + contentType = contentType + ) + val idByKorean = themesWithIds.associate { it.theme to it.id } + + val themeIds = idByKorean.values.distinct() + val translatedById = if (themeIds.isNotEmpty()) { + contentThemeTranslationRepository + .findByContentThemeIdInAndLocale(themeIds, locale) + .associate { it.contentThemeId to it.theme } + } else { + emptyMap() + } + + translatedContentList.map { item -> + val themeId = idByKorean[item.themeStr] + val translated = if (themeId != null) translatedById[themeId] else null + if (!translated.isNullOrBlank()) item.copy(themeStr = translated) else item + } + } + } + return GetAudioContentListResponse( totalCount = totalCount, - items = translatedContentList + items = themeTranslatedList ) }