Compare commits
3 Commits
31242a1f76
...
ee495dae3a
| Author | SHA1 | Date | |
|---|---|---|---|
| ee495dae3a | |||
| 3f74eefacc | |||
| 6cc15a8748 |
37
AGENTS.md
Normal file
37
AGENTS.md
Normal file
@@ -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와 사람이 동일한 개발 규칙을 공유하기 위한 최상위 기준 문서**이며,
|
||||
✅ 모든 신규 코드는 본 문서를 기준으로 검토된다.
|
||||
@@ -835,9 +835,9 @@ class AudioContentService(
|
||||
val theme = audioContent.theme
|
||||
if (theme?.id != null) {
|
||||
val locale = langContext.lang.code
|
||||
val translated = contentThemeTranslationRepository
|
||||
val translatedContentTheme = contentThemeTranslationRepository
|
||||
.findByContentThemeIdAndLocale(theme.id!!, locale)
|
||||
val text = translated?.theme
|
||||
val text = translatedContentTheme?.theme
|
||||
if (!text.isNullOrBlank()) text else theme.theme
|
||||
} else {
|
||||
audioContent.theme!!.theme
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ data class CreatorCommunity(
|
||||
audioUrl: String?,
|
||||
content: String,
|
||||
date: String,
|
||||
dateUtc: String,
|
||||
isLike: Boolean,
|
||||
existOrdered: Boolean,
|
||||
likeCount: Int,
|
||||
@@ -51,6 +52,7 @@ data class CreatorCommunity(
|
||||
content = content,
|
||||
price = price,
|
||||
date = date,
|
||||
dateUtc = dateUtc,
|
||||
isCommentAvailable = isCommentAvailable,
|
||||
isAdult = false,
|
||||
isLike = isLike,
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.ZoneId
|
||||
|
||||
@Service
|
||||
class CreatorCommunityService(
|
||||
@@ -243,6 +244,10 @@ class CreatorCommunityService(
|
||||
imageHost = imageHost,
|
||||
audioUrl = audioUrl,
|
||||
date = it.date.getTimeAgoString(),
|
||||
dateUtc = it.date
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.toInstant()
|
||||
.toString(),
|
||||
isLike = isLike,
|
||||
memberId = memberId,
|
||||
existOrdered = if (memberId == it.creatorId) {
|
||||
@@ -314,6 +319,10 @@ class CreatorCommunityService(
|
||||
imageHost = imageHost,
|
||||
audioUrl = audioUrl,
|
||||
date = post.date.getTimeAgoString(),
|
||||
dateUtc = post.date
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.toInstant()
|
||||
.toString(),
|
||||
isLike = isLike,
|
||||
memberId = memberId,
|
||||
existOrdered = if (memberId == post.creatorId) {
|
||||
@@ -494,6 +503,10 @@ class CreatorCommunityService(
|
||||
imageHost = imageHost,
|
||||
audioUrl = null,
|
||||
date = it.date.getTimeAgoString(),
|
||||
dateUtc = it.date
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.toInstant()
|
||||
.toString(),
|
||||
isLike = isLike,
|
||||
memberId = memberId,
|
||||
existOrdered = if (memberId == it.creatorId) {
|
||||
@@ -578,6 +591,10 @@ class CreatorCommunityService(
|
||||
audioUrl = audioUrl,
|
||||
content = post.content,
|
||||
date = post.createdAt!!.getTimeAgoString(),
|
||||
dateUtc = post.createdAt!!
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.toInstant()
|
||||
.toString(),
|
||||
isLike = isLike,
|
||||
existOrdered = true,
|
||||
likeCount = likeCount,
|
||||
|
||||
@@ -13,6 +13,7 @@ data class GetCommunityPostListResponse @QueryProjection constructor(
|
||||
val content: String,
|
||||
val price: Int,
|
||||
val date: String,
|
||||
val dateUtc: String,
|
||||
val isCommentAvailable: Boolean,
|
||||
val isAdult: Boolean,
|
||||
val isLike: Boolean,
|
||||
|
||||
@@ -21,6 +21,7 @@ data class SelectCommunityPostResponse @QueryProjection constructor(
|
||||
imageHost: String,
|
||||
audioUrl: String?,
|
||||
date: String,
|
||||
dateUtc: String,
|
||||
isLike: Boolean,
|
||||
memberId: Long,
|
||||
existOrdered: Boolean,
|
||||
@@ -57,6 +58,7 @@ data class SelectCommunityPostResponse @QueryProjection constructor(
|
||||
},
|
||||
price = price,
|
||||
date = date,
|
||||
dateUtc = dateUtc,
|
||||
isCommentAvailable = isCommentAvailable,
|
||||
isAdult = isAdult,
|
||||
isLike = isLike,
|
||||
|
||||
Reference in New Issue
Block a user