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
|
val theme = audioContent.theme
|
||||||
if (theme?.id != null) {
|
if (theme?.id != null) {
|
||||||
val locale = langContext.lang.code
|
val locale = langContext.lang.code
|
||||||
val translated = contentThemeTranslationRepository
|
val translatedContentTheme = contentThemeTranslationRepository
|
||||||
.findByContentThemeIdAndLocale(theme.id!!, locale)
|
.findByContentThemeIdAndLocale(theme.id!!, locale)
|
||||||
val text = translated?.theme
|
val text = translatedContentTheme?.theme
|
||||||
if (!text.isNullOrBlank()) text else theme.theme
|
if (!text.isNullOrBlank()) text else theme.theme
|
||||||
} else {
|
} else {
|
||||||
audioContent.theme!!.theme
|
audioContent.theme!!.theme
|
||||||
@@ -920,12 +920,34 @@ class AudioContentService(
|
|||||||
contentId = audioContent.id!!
|
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(
|
return GetAudioContentListItem(
|
||||||
contentId = audioContent.id!!,
|
contentId = audioContent.id!!,
|
||||||
coverImageUrl = "$coverImageHost/${audioContent.coverImage}",
|
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,
|
price = audioContent.price,
|
||||||
themeStr = audioContent.theme!!.theme,
|
themeStr = themeStrTranslated,
|
||||||
duration = audioContent.duration,
|
duration = audioContent.duration,
|
||||||
likeCount = likeCount,
|
likeCount = likeCount,
|
||||||
commentCount = commentCount,
|
commentCount = commentCount,
|
||||||
@@ -1017,9 +1039,42 @@ class AudioContentService(
|
|||||||
items
|
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(
|
return GetAudioContentListResponse(
|
||||||
totalCount = totalCount,
|
totalCount = totalCount,
|
||||||
items = translatedContentList
|
items = themeTranslatedList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ data class CreatorCommunity(
|
|||||||
audioUrl: String?,
|
audioUrl: String?,
|
||||||
content: String,
|
content: String,
|
||||||
date: String,
|
date: String,
|
||||||
|
dateUtc: String,
|
||||||
isLike: Boolean,
|
isLike: Boolean,
|
||||||
existOrdered: Boolean,
|
existOrdered: Boolean,
|
||||||
likeCount: Int,
|
likeCount: Int,
|
||||||
@@ -51,6 +52,7 @@ data class CreatorCommunity(
|
|||||||
content = content,
|
content = content,
|
||||||
price = price,
|
price = price,
|
||||||
date = date,
|
date = date,
|
||||||
|
dateUtc = dateUtc,
|
||||||
isCommentAvailable = isCommentAvailable,
|
isCommentAvailable = isCommentAvailable,
|
||||||
isAdult = false,
|
isAdult = false,
|
||||||
isLike = isLike,
|
isLike = isLike,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.springframework.data.repository.findByIdOrNull
|
|||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.springframework.web.multipart.MultipartFile
|
import org.springframework.web.multipart.MultipartFile
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class CreatorCommunityService(
|
class CreatorCommunityService(
|
||||||
@@ -243,6 +244,10 @@ class CreatorCommunityService(
|
|||||||
imageHost = imageHost,
|
imageHost = imageHost,
|
||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
date = it.date.getTimeAgoString(),
|
date = it.date.getTimeAgoString(),
|
||||||
|
dateUtc = it.date
|
||||||
|
.atZone(ZoneId.of("UTC"))
|
||||||
|
.toInstant()
|
||||||
|
.toString(),
|
||||||
isLike = isLike,
|
isLike = isLike,
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
existOrdered = if (memberId == it.creatorId) {
|
existOrdered = if (memberId == it.creatorId) {
|
||||||
@@ -314,6 +319,10 @@ class CreatorCommunityService(
|
|||||||
imageHost = imageHost,
|
imageHost = imageHost,
|
||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
date = post.date.getTimeAgoString(),
|
date = post.date.getTimeAgoString(),
|
||||||
|
dateUtc = post.date
|
||||||
|
.atZone(ZoneId.of("UTC"))
|
||||||
|
.toInstant()
|
||||||
|
.toString(),
|
||||||
isLike = isLike,
|
isLike = isLike,
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
existOrdered = if (memberId == post.creatorId) {
|
existOrdered = if (memberId == post.creatorId) {
|
||||||
@@ -494,6 +503,10 @@ class CreatorCommunityService(
|
|||||||
imageHost = imageHost,
|
imageHost = imageHost,
|
||||||
audioUrl = null,
|
audioUrl = null,
|
||||||
date = it.date.getTimeAgoString(),
|
date = it.date.getTimeAgoString(),
|
||||||
|
dateUtc = it.date
|
||||||
|
.atZone(ZoneId.of("UTC"))
|
||||||
|
.toInstant()
|
||||||
|
.toString(),
|
||||||
isLike = isLike,
|
isLike = isLike,
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
existOrdered = if (memberId == it.creatorId) {
|
existOrdered = if (memberId == it.creatorId) {
|
||||||
@@ -578,6 +591,10 @@ class CreatorCommunityService(
|
|||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
content = post.content,
|
content = post.content,
|
||||||
date = post.createdAt!!.getTimeAgoString(),
|
date = post.createdAt!!.getTimeAgoString(),
|
||||||
|
dateUtc = post.createdAt!!
|
||||||
|
.atZone(ZoneId.of("UTC"))
|
||||||
|
.toInstant()
|
||||||
|
.toString(),
|
||||||
isLike = isLike,
|
isLike = isLike,
|
||||||
existOrdered = true,
|
existOrdered = true,
|
||||||
likeCount = likeCount,
|
likeCount = likeCount,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ data class GetCommunityPostListResponse @QueryProjection constructor(
|
|||||||
val content: String,
|
val content: String,
|
||||||
val price: Int,
|
val price: Int,
|
||||||
val date: String,
|
val date: String,
|
||||||
|
val dateUtc: String,
|
||||||
val isCommentAvailable: Boolean,
|
val isCommentAvailable: Boolean,
|
||||||
val isAdult: Boolean,
|
val isAdult: Boolean,
|
||||||
val isLike: Boolean,
|
val isLike: Boolean,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ data class SelectCommunityPostResponse @QueryProjection constructor(
|
|||||||
imageHost: String,
|
imageHost: String,
|
||||||
audioUrl: String?,
|
audioUrl: String?,
|
||||||
date: String,
|
date: String,
|
||||||
|
dateUtc: String,
|
||||||
isLike: Boolean,
|
isLike: Boolean,
|
||||||
memberId: Long,
|
memberId: Long,
|
||||||
existOrdered: Boolean,
|
existOrdered: Boolean,
|
||||||
@@ -57,6 +58,7 @@ data class SelectCommunityPostResponse @QueryProjection constructor(
|
|||||||
},
|
},
|
||||||
price = price,
|
price = price,
|
||||||
date = date,
|
date = date,
|
||||||
|
dateUtc = dateUtc,
|
||||||
isCommentAvailable = isCommentAvailable,
|
isCommentAvailable = isCommentAvailable,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
isLike = isLike,
|
isLike = isLike,
|
||||||
|
|||||||
Reference in New Issue
Block a user