Compare commits
3 Commits
92fe6caf17
...
1240f00ea2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1240f00ea2 | |||
| 2395c7c208 | |||
| 37ad325cc2 |
@@ -339,6 +339,33 @@ spring:
|
|||||||
- 어떻게: `src/main/resources/application.yml`, `src/test/resources/application.yml`의 `spring.jpa` 아래에 `open-in-view: false`를 추가했다.
|
- 어떻게: `src/main/resources/application.yml`, `src/test/resources/application.yml`의 `spring.jpa` 아래에 `open-in-view: false`를 추가했다.
|
||||||
- 결과: 확인된 lazy loading 재현 실패가 없어 production code의 service/repository/controller 수정은 하지 않았다. 공개 API 스키마 변경도 없다.
|
- 결과: 확인된 lazy loading 재현 실패가 없어 production code의 service/repository/controller 수정은 하지 않았다. 공개 API 스키마 변경도 없다.
|
||||||
|
|
||||||
|
- [x] **Task 0.5: 운영 LazyInitializationException 회귀 보완**
|
||||||
|
- Files:
|
||||||
|
- Add: `src/test/kotlin/kr/co/vividnext/sodalive/osiv/OsivLazyLoadingRegressionTest.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt`
|
||||||
|
- Modify: `docs/20260618_유저크리에이터채팅_WebSocket전환/prd.md`
|
||||||
|
- Modify: `docs/20260618_유저크리에이터채팅_WebSocket전환/plan-task.md`
|
||||||
|
- RED: `ChatCharacterService.getCharacterDetail` 반환 후 `tagMappings.tag.tag`, `getOtherCharactersBySharedTags` 반환 후 `tagMappings.tag.tag`, `RankingRepository.getCreatorRankings` 반환 후 `Member.toExplorerSectionCreator`를 트랜잭션 밖에서 접근하는 테스트를 추가한다.
|
||||||
|
- 실패 확인:
|
||||||
|
- Run: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process -Dspring.jpa.open-in-view=false --tests kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest`
|
||||||
|
- Expected: 기존 코드에서는 `LazyInitializationException` 또는 동등한 실패가 발생한다.
|
||||||
|
- GREEN: 응답 조립에 필요한 `ChatCharacter.tagMappings.tag`, `Member.tags.tag`를 조회 쿼리에서 fetch join으로 선로딩한다.
|
||||||
|
- 통과 확인:
|
||||||
|
- Run: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process -Dspring.jpa.open-in-view=false --tests kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest`
|
||||||
|
- Expected: `BUILD SUCCESSFUL`
|
||||||
|
- REFACTOR: 공개 API 응답 스키마와 WebSocket 관련 구현은 변경하지 않는다.
|
||||||
|
- 검증 기록:
|
||||||
|
- 무엇: OSIV off 상태에서 운영 오류와 같은 lazy loading 경계를 재현하는 회귀 테스트를 추가하고, 필요한 연관을 fetch join으로 선로딩했다.
|
||||||
|
- 왜: `ChatCharacterController.getCharacterDetail`에서 `ChatCharacterTagMapping.tag`, `HomeService.fetchData`에서 `Member.tags`가 트랜잭션 밖에서 열려 `LazyInitializationException`이 발생했기 때문이다.
|
||||||
|
- 어떻게: `OsivLazyLoadingRegressionTest`를 추가해 `ChatCharacterService.getCharacterDetail`, `ChatCharacterService.getOtherCharactersBySharedTags`, `RankingRepository.getCreatorRankings` 반환 후 트랜잭션 밖 DTO 변환을 검증했다.
|
||||||
|
- RED: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process -Dspring.jpa.open-in-view=false --tests kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest` 실행 결과 3개 테스트 모두 `LazyInitializationException`으로 실패했다.
|
||||||
|
- GREEN: 같은 명령을 재실행해 `BUILD SUCCESSFUL in 1m 6s`로 통과했다.
|
||||||
|
- 인접 회귀: `./gradlew --no-daemon cleanTest test -Dkotlin.compiler.execution.strategy=in-process -Dspring.jpa.open-in-view=false --tests kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest --tests kr.co.vividnext.sodalive.api.home.HomeServiceTest --tests kr.co.vividnext.sodalive.chat.character.controller.ChatCharacterControllerTest`가 `BUILD SUCCESSFUL in 24s`로 통과했다.
|
||||||
|
- 전체 테스트 중단: `./gradlew --no-daemon test -Dkotlin.compiler.execution.strategy=in-process -Dspring.jpa.open-in-view=false`는 `UserCreatorChatRedisIntegrationTest` 실행 중 `OutOfMemoryError`가 발생해 즉시 중단했다. 이후 검증 범위는 OSIV 회귀와 인접 테스트로 간결화했다.
|
||||||
|
- lint: `./gradlew --no-daemon ktlintCheck`가 `BUILD SUCCESSFUL in 14s`로 통과했다.
|
||||||
|
- 정적 점검: `rg -n "toExplorerSectionCreator\\(|tagMappings\\.map|tagMappings\\.joinToString|\\.tagMappings" src/main/kotlin/kr/co/vividnext/sodalive -S`로 동일 패턴 후보를 확인했다. `ExplorerService`는 클래스 단위 `@Transactional(readOnly = true)` 안에서 변환하고, `HomeService`/`RankingService`는 공통 `RankingRepository.getCreatorRankings` 선로딩으로 보완했다. `TranslationSourceExtractor`와 관리자/원작 DTO 변환의 `tagMappings` 접근은 운영 stacktrace 표면이 아니므로 별도 회귀 후보로 남겼다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 1: WebSocket 의존성과 인증 handshake 기반 추가
|
### Phase 1: WebSocket 의존성과 인증 handshake 기반 추가
|
||||||
@@ -916,3 +943,10 @@ spring:
|
|||||||
- OSIV off 테스트: Phase 0 묶음 검증 명령에 포함
|
- OSIV off 테스트: Phase 0 묶음 검증 명령에 포함
|
||||||
- 수정 방향: JPA lazy loading 직접 검증은 아니며, WebMvc 표면 회귀만 확인
|
- 수정 방향: JPA lazy loading 직접 검증은 아니며, WebMvc 표면 회귀만 확인
|
||||||
- 처리 상태: XML 기준 `CreatorChannelHomeControllerTest` 2개, `CreatorChannelLiveControllerTest` 5개 모두 `failures=0`, `errors=0`
|
- 처리 상태: XML 기준 `CreatorChannelHomeControllerTest` 2개, `CreatorChannelLiveControllerTest` 5개 모두 `failures=0`, `errors=0`
|
||||||
|
- API/기능: 캐릭터 상세/홈 크리에이터 랭킹 운영 회귀
|
||||||
|
- 파일: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt`
|
||||||
|
- 위험 유형: service/repository 반환 후 controller/service DTO 변환 중 nested lazy proxy 접근
|
||||||
|
- lazy 접근 대상: `ChatCharacter.tagMappings.tag`, `Member.tags.tag`
|
||||||
|
- OSIV off 테스트: `OsivLazyLoadingRegressionTest`
|
||||||
|
- 수정 방향: 상세/공유 태그 캐릭터 조회와 크리에이터 랭킹 조회에서 필요한 연관을 fetch join으로 선로딩
|
||||||
|
- 처리 상태: `OsivLazyLoadingRegressionTest` 3개 모두 통과
|
||||||
|
|||||||
@@ -235,6 +235,20 @@
|
|||||||
- 관리자/크리에이터/사용자 API가 서로 다른 controller 패키지에 흩어져 있으므로 특정 패키지 검색만으로 점검을 끝내지 않는다.
|
- 관리자/크리에이터/사용자 API가 서로 다른 controller 패키지에 흩어져 있으므로 특정 패키지 검색만으로 점검을 끝내지 않는다.
|
||||||
- OSIV off 적용 후 일부 API가 실패하면 WebSocket 전환과 섞어 수정하지 않고, lazy loading 제거 task로 분리해 먼저 처리한다.
|
- OSIV off 적용 후 일부 API가 실패하면 WebSocket 전환과 섞어 수정하지 않고, lazy loading 제거 task로 분리해 먼저 처리한다.
|
||||||
|
|
||||||
|
### Feature H. OSIV off lazy loading 회귀 보완
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- 운영에서 확인된 `LazyInitializationException` 발생 지점을 우선 수정한다.
|
||||||
|
- `ChatCharacterController.getCharacterDetail` 응답 조립에 필요한 `ChatCharacter.tagMappings.tag`는 OSIV off 상태에서도 접근 가능해야 한다.
|
||||||
|
- `HomeService.fetchData`의 크리에이터 랭킹 응답 조립에 필요한 `Member.tags.tag`는 OSIV off 상태에서도 접근 가능해야 한다.
|
||||||
|
- 동일 변환 메서드(`toExplorerSectionCreator`)를 쓰는 기존 랭킹 조회도 같은 쿼리 선로딩 정책을 공유해야 한다.
|
||||||
|
- 공개 API 응답 스키마는 변경하지 않는다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 컬렉션 크기만 접근하면 nested LAZY proxy(`mapping.tag`)는 초기화되지 않을 수 있다.
|
||||||
|
- 조회 테스트에 `@Transactional`이 붙어 있으면 서비스 반환 후 lazy 접근 실패를 가릴 수 있다.
|
||||||
|
- fetch join으로 one-to-many를 가져오면 중복 row가 생길 수 있으므로 결과 중복 여부를 검증한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. UX / UI Expectations
|
## 8. UX / UI Expectations
|
||||||
|
|||||||
@@ -196,8 +196,12 @@ class AdminOriginalWorkService(
|
|||||||
/** 원작 상세 조회 (소프트 삭제 제외) */
|
/** 원작 상세 조회 (소프트 삭제 제외) */
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getOriginalWork(id: Long): OriginalWork {
|
fun getOriginalWork(id: Long): OriginalWork {
|
||||||
return originalWorkRepository.findByIdAndIsDeletedFalse(id)
|
val originalWork = originalWorkRepository.findByIdAndIsDeletedFalse(id)
|
||||||
.orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
|
.orElseThrow { SodaException(messageKey = "admin.chat.original.not_found") }
|
||||||
|
|
||||||
|
initializeResponseRelations(originalWork)
|
||||||
|
|
||||||
|
return originalWork
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 원작 페이징 조회 */
|
/** 원작 페이징 조회 */
|
||||||
@@ -210,7 +214,9 @@ class AdminOriginalWorkService(
|
|||||||
else -> size
|
else -> size
|
||||||
}
|
}
|
||||||
val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending())
|
val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending())
|
||||||
return originalWorkRepository.findByIsDeletedFalse(pageable)
|
val originalWorks = originalWorkRepository.findByIsDeletedFalse(pageable)
|
||||||
|
originalWorks.content.forEach { initializeResponseRelations(it) }
|
||||||
|
return originalWorks
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 지정 원작에 속한 활성 캐릭터 페이징 조회 (최신순) */
|
/** 지정 원작에 속한 활성 캐릭터 페이징 조회 (최신순) */
|
||||||
@@ -233,7 +239,14 @@ class AdminOriginalWorkService(
|
|||||||
/** 원작 검색 (제목/콘텐츠타입/카테고리, 소프트 삭제 제외) - 무페이징 */
|
/** 원작 검색 (제목/콘텐츠타입/카테고리, 소프트 삭제 제외) - 무페이징 */
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun searchOriginalWorksAll(searchTerm: String): List<OriginalWork> {
|
fun searchOriginalWorksAll(searchTerm: String): List<OriginalWork> {
|
||||||
return originalWorkRepository.searchNoPaging(searchTerm)
|
val originalWorks = originalWorkRepository.searchNoPaging(searchTerm)
|
||||||
|
originalWorks.forEach { initializeResponseRelations(it) }
|
||||||
|
return originalWorks
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeResponseRelations(originalWork: OriginalWork) {
|
||||||
|
originalWork.originalLinks.forEach { it.url }
|
||||||
|
originalWork.tagMappings.forEach { it.tag.tag }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 원작에 기존 캐릭터들을 배정 */
|
/** 원작에 기존 캐릭터들을 배정 */
|
||||||
|
|||||||
@@ -74,6 +74,28 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
|||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): List<ChatCharacter>
|
): List<ChatCharacter>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터 ID를 무작위로 조회 (현재 캐릭터 제외)
|
||||||
|
*/
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT c.id FROM ChatCharacter c
|
||||||
|
JOIN c.tagMappings tm
|
||||||
|
JOIN tm.tag t
|
||||||
|
WHERE c.isActive = true
|
||||||
|
AND c.id <> :characterId
|
||||||
|
AND t.id IN (
|
||||||
|
SELECT t2.id FROM ChatCharacterTagMapping tm2 JOIN tm2.tag t2 WHERE tm2.chatCharacter.id = :characterId
|
||||||
|
)
|
||||||
|
GROUP BY c.id
|
||||||
|
ORDER BY function('RAND')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun findRandomIdsBySharedTags(
|
||||||
|
@Param("characterId") characterId: Long,
|
||||||
|
pageable: Pageable
|
||||||
|
): List<Long>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 활성 캐릭터 무작위 조회
|
* 활성 캐릭터 무작위 조회
|
||||||
*/
|
*/
|
||||||
@@ -99,6 +121,27 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
|||||||
fun findRandomActiveExcluding(@Param("excludeIds") excludeIds: List<Long>, pageable: Pageable): List<ChatCharacter>
|
fun findRandomActiveExcluding(@Param("excludeIds") excludeIds: List<Long>, pageable: Pageable): List<ChatCharacter>
|
||||||
|
|
||||||
fun findByIdInAndIsActiveTrue(ids: List<Long>): List<ChatCharacter>
|
fun findByIdInAndIsActiveTrue(ids: List<Long>): List<ChatCharacter>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT c FROM ChatCharacter c
|
||||||
|
LEFT JOIN FETCH c.tagMappings tm
|
||||||
|
LEFT JOIN FETCH tm.tag
|
||||||
|
WHERE c.id = :id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun findByIdWithTagMappings(@Param("id") id: Long): ChatCharacter?
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT c FROM ChatCharacter c
|
||||||
|
LEFT JOIN FETCH c.tagMappings tm
|
||||||
|
LEFT JOIN FETCH tm.tag
|
||||||
|
WHERE c.id IN :ids
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun findByIdInWithTagMappings(@Param("ids") ids: List<Long>): List<ChatCharacter>
|
||||||
|
|
||||||
fun findByCreatorMemberId(creatorMemberId: Long): ChatCharacter?
|
fun findByCreatorMemberId(creatorMemberId: Long): ChatCharacter?
|
||||||
fun existsByCreatorMemberId(creatorMemberId: Long): Boolean
|
fun existsByCreatorMemberId(creatorMemberId: Long): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,13 +210,15 @@ class ChatCharacterService(
|
|||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getOtherCharactersBySharedTags(characterId: Long, limit: Int = 10): List<ChatCharacter> {
|
fun getOtherCharactersBySharedTags(characterId: Long, limit: Int = 10): List<ChatCharacter> {
|
||||||
val others = chatCharacterRepository.findRandomBySharedTags(
|
val ids = chatCharacterRepository.findRandomIdsBySharedTags(
|
||||||
characterId,
|
characterId,
|
||||||
PageRequest.of(0, limit)
|
PageRequest.of(0, limit)
|
||||||
)
|
).distinct()
|
||||||
// 태그 초기화 (지연 로딩 문제 방지)
|
if (ids.isEmpty()) return emptyList()
|
||||||
others.forEach { it.tagMappings.size }
|
|
||||||
return others
|
val charactersById = chatCharacterRepository.findByIdInWithTagMappings(ids)
|
||||||
|
.associateBy { it.id }
|
||||||
|
return ids.mapNotNull { charactersById[it] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -555,13 +557,12 @@ class ChatCharacterService(
|
|||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getCharacterDetail(id: Long): ChatCharacter? {
|
fun getCharacterDetail(id: Long): ChatCharacter? {
|
||||||
val character = findById(id) ?: return null
|
val character = chatCharacterRepository.findByIdWithTagMappings(id) ?: return null
|
||||||
|
|
||||||
// 지연 로딩된 관계 데이터 초기화
|
// 지연 로딩된 관계 데이터 초기화
|
||||||
character.tagMappings.size
|
character.valueMappings.forEach { it.value.value }
|
||||||
character.valueMappings.size
|
character.hobbyMappings.forEach { it.hobby.hobby }
|
||||||
character.hobbyMappings.size
|
character.goalMappings.forEach { it.goal.goal }
|
||||||
character.goalMappings.size
|
|
||||||
character.memories.size
|
character.memories.size
|
||||||
character.personalities.size
|
character.personalities.size
|
||||||
character.backgrounds.size
|
character.backgrounds.size
|
||||||
|
|||||||
@@ -43,8 +43,13 @@ class OriginalWorkQueryService(
|
|||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getOriginalWork(id: Long): OriginalWork {
|
fun getOriginalWork(id: Long): OriginalWork {
|
||||||
return originalWorkRepository.findByIdAndIsDeletedFalse(id)
|
val originalWork = originalWorkRepository.findByIdAndIsDeletedFalse(id)
|
||||||
.orElseThrow { SodaException(messageKey = "chat.original.not_found") }
|
.orElseThrow { SodaException(messageKey = "chat.original.not_found") }
|
||||||
|
|
||||||
|
originalWork.originalLinks.forEach { it.url }
|
||||||
|
originalWork.tagMappings.forEach { it.tag.tag }
|
||||||
|
|
||||||
|
return originalWork
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package kr.co.vividnext.sodalive.i18n.translation
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService.Companion.getTranslatableLanguageCodes
|
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService.Companion.getTranslatableLanguageCodes
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class ResourceTranslationJobScheduler(
|
class ResourceTranslationJobScheduler(
|
||||||
private val sourceExtractor: TranslationSourceExtractor,
|
private val sourceExtractor: TranslationSourceExtractor,
|
||||||
private val translationJobScheduler: TranslationJobScheduler
|
private val translationJobScheduler: TranslationJobScheduler
|
||||||
) {
|
) {
|
||||||
|
@Transactional
|
||||||
fun scheduleResourceTranslations(resourceType: LanguageTranslationTargetType, resourceId: Long) {
|
fun scheduleResourceTranslations(resourceType: LanguageTranslationTargetType, resourceId: Long) {
|
||||||
val source = sourceExtractor.extract(resourceType, resourceId) ?: return
|
val source = sourceExtractor.extract(resourceType, resourceId) ?: return
|
||||||
getTranslatableLanguageCodes(source.sourceLanguage).forEach { targetLanguage ->
|
getTranslatableLanguageCodes(source.sourceLanguage).forEach { targetLanguage ->
|
||||||
@@ -15,6 +17,7 @@ class ResourceTranslationJobScheduler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
fun scheduleResourceTranslation(
|
fun scheduleResourceTranslation(
|
||||||
resourceType: LanguageTranslationTargetType,
|
resourceType: LanguageTranslationTargetType,
|
||||||
resourceId: Long,
|
resourceId: Long,
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import kr.co.vividnext.sodalive.member.Member
|
|||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
import kr.co.vividnext.sodalive.member.QMember.member
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
|
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
|
||||||
|
import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag
|
||||||
|
import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -53,6 +55,8 @@ class RankingRepository(
|
|||||||
.select(member)
|
.select(member)
|
||||||
.from(creatorRanking)
|
.from(creatorRanking)
|
||||||
.innerJoin(creatorRanking.member, member)
|
.innerJoin(creatorRanking.member, member)
|
||||||
|
.leftJoin(member.tags, memberCreatorTag).fetchJoin()
|
||||||
|
.leftJoin(memberCreatorTag.tag, creatorTag).fetchJoin()
|
||||||
|
|
||||||
if (memberId != null) {
|
if (memberId != null) {
|
||||||
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
@@ -65,6 +69,7 @@ class RankingRepository(
|
|||||||
return select
|
return select
|
||||||
.orderBy(creatorRanking.ranking.asc())
|
.orderBy(creatorRanking.ranking.asc())
|
||||||
.fetch()
|
.fetch()
|
||||||
|
.distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAudioContentRanking(
|
fun getAudioContentRanking(
|
||||||
|
|||||||
@@ -0,0 +1,448 @@
|
|||||||
|
package kr.co.vividnext.sodalive.osiv
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkResponse
|
||||||
|
import kr.co.vividnext.sodalive.admin.chat.original.service.AdminOriginalWorkService
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.image.CharacterImageRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterGoalRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterHobbyRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterTagRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterValueRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberService
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.service.PopularCharacterQuery
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkLink
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTag
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTagMapping
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkDetailResponse
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.repository.OriginalWorkTagRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.service.OriginalWorkQueryService
|
||||||
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
|
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
||||||
|
import kr.co.vividnext.sodalive.explorer.CreatorRanking
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobRepository
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.TranslationJobScheduler
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.TranslationSourceExtractor
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.tag.CreatorTag
|
||||||
|
import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag
|
||||||
|
import kr.co.vividnext.sodalive.rank.RankingRepository
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import org.springframework.transaction.annotation.Propagation
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
|
@DataJpaTest(
|
||||||
|
properties = [
|
||||||
|
"spring.cache.type=none",
|
||||||
|
"spring.datasource.url=jdbc:h2:mem:osiv-regression;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||||
|
@Import(
|
||||||
|
QueryDslConfig::class,
|
||||||
|
ResourceTranslationJobScheduler::class,
|
||||||
|
TranslationSourceExtractor::class,
|
||||||
|
TranslationJobScheduler::class,
|
||||||
|
AudioContentThemeQueryRepository::class
|
||||||
|
)
|
||||||
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||||
|
class OsivLazyLoadingRegressionTest @Autowired constructor(
|
||||||
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
|
private val tagRepository: ChatCharacterTagRepository,
|
||||||
|
private val valueRepository: ChatCharacterValueRepository,
|
||||||
|
private val hobbyRepository: ChatCharacterHobbyRepository,
|
||||||
|
private val goalRepository: ChatCharacterGoalRepository,
|
||||||
|
private val originalWorkRepository: OriginalWorkRepository,
|
||||||
|
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler,
|
||||||
|
private val translationJobRepository: TranslationJobRepository,
|
||||||
|
private val queryFactory: JPAQueryFactory,
|
||||||
|
private val transactionTemplate: TransactionTemplate,
|
||||||
|
private val entityManager: EntityManager
|
||||||
|
) {
|
||||||
|
private val chatCharacterService = ChatCharacterService(
|
||||||
|
chatCharacterRepository = chatCharacterRepository,
|
||||||
|
tagRepository = tagRepository,
|
||||||
|
valueRepository = valueRepository,
|
||||||
|
hobbyRepository = hobbyRepository,
|
||||||
|
goalRepository = goalRepository,
|
||||||
|
popularCharacterQuery = Mockito.mock(PopularCharacterQuery::class.java),
|
||||||
|
imageRepository = Mockito.mock(CharacterImageRepository::class.java),
|
||||||
|
creatorMemberService = Mockito.mock(ChatCharacterCreatorMemberService::class.java),
|
||||||
|
imageHost = "https://cdn.test"
|
||||||
|
)
|
||||||
|
private val rankingRepository = RankingRepository(queryFactory, "https://cdn.test")
|
||||||
|
private val originalWorkQueryService = OriginalWorkQueryService(
|
||||||
|
originalWorkRepository = originalWorkRepository,
|
||||||
|
chatCharacterRepository = chatCharacterRepository
|
||||||
|
)
|
||||||
|
private val adminOriginalWorkService = AdminOriginalWorkService(
|
||||||
|
originalWorkRepository = originalWorkRepository,
|
||||||
|
chatCharacterRepository = chatCharacterRepository,
|
||||||
|
originalWorkTagRepository = Mockito.mock(OriginalWorkTagRepository::class.java),
|
||||||
|
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("캐릭터 상세 조회 결과는 트랜잭션 밖에서도 태그 이름을 읽을 수 있다")
|
||||||
|
fun shouldLoadCharacterDetailTagOutsideTransaction() {
|
||||||
|
val characterId = saveCharacterWithTag("detail")
|
||||||
|
|
||||||
|
val character = transactionTemplate.execute {
|
||||||
|
chatCharacterService.getCharacterDetail(characterId)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
val tags = character.tagMappings.map { it.tag.tag }
|
||||||
|
assertEquals(listOf("detail-tag"), tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("캐릭터 상세 조회 결과는 트랜잭션 밖에서도 가치관, 취미, 목표 이름을 읽을 수 있다")
|
||||||
|
fun shouldLoadCharacterDetailMappingTargetsOutsideTransaction() {
|
||||||
|
val characterId = saveCharacterWithDetailMappings("detail-target")
|
||||||
|
|
||||||
|
val character = transactionTemplate.execute {
|
||||||
|
chatCharacterService.getCharacterDetail(characterId)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
assertEquals(listOf("detail-target-value"), character.valueMappings.map { it.value.value })
|
||||||
|
assertEquals(listOf("detail-target-hobby"), character.hobbyMappings.map { it.hobby.hobby })
|
||||||
|
assertEquals(listOf("detail-target-goal"), character.goalMappings.map { it.goal.goal })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("공유 태그 기반 다른 캐릭터 조회 결과는 트랜잭션 밖에서도 태그 이름을 읽을 수 있다")
|
||||||
|
fun shouldLoadSharedTagCharactersOutsideTransaction() {
|
||||||
|
val characterId = saveTwoCharactersWithSharedTag()
|
||||||
|
|
||||||
|
val characters = transactionTemplate.execute {
|
||||||
|
chatCharacterService.getOtherCharactersBySharedTags(characterId, 10)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
val tags = characters.single().tagMappings.map { it.tag.tag }
|
||||||
|
assertEquals(listOf("shared-tag"), tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("공유 태그 기반 다른 캐릭터 ID 조회 결과는 태그 조인 중복을 제거한다")
|
||||||
|
fun shouldReturnDistinctSharedTagCharacterIds() {
|
||||||
|
val characterId = saveCharactersWithDuplicateSharedTags()
|
||||||
|
|
||||||
|
val ids = transactionTemplate.execute {
|
||||||
|
chatCharacterRepository.findRandomIdsBySharedTags(
|
||||||
|
characterId,
|
||||||
|
PageRequest.of(0, 10)
|
||||||
|
)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
assertEquals(ids.distinct(), ids)
|
||||||
|
assertEquals(2, ids.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("원작 상세 조회 결과는 트랜잭션 밖에서도 원작 링크와 태그 이름을 DTO로 변환할 수 있다")
|
||||||
|
fun shouldLoadOriginalWorkDetailRelationsOutsideTransaction() {
|
||||||
|
val originalWorkId = saveOriginalWorkWithLinkAndTag()
|
||||||
|
|
||||||
|
val originalWork = transactionTemplate.execute {
|
||||||
|
originalWorkQueryService.getOriginalWork(originalWorkId)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
val response = OriginalWorkDetailResponse.from(
|
||||||
|
entity = originalWork,
|
||||||
|
characters = emptyList(),
|
||||||
|
translated = null
|
||||||
|
)
|
||||||
|
assertEquals(listOf("https://original.test/original"), response.originalLinks)
|
||||||
|
assertEquals(listOf("original-tag"), response.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자 원작 상세 조회 결과는 트랜잭션 밖에서도 DTO로 변환할 수 있다")
|
||||||
|
fun shouldLoadAdminOriginalWorkDetailRelationsOutsideTransaction() {
|
||||||
|
val originalWorkId = saveOriginalWorkWithLinkAndTag("admin-detail")
|
||||||
|
|
||||||
|
val originalWork = transactionTemplate.execute {
|
||||||
|
adminOriginalWorkService.getOriginalWork(originalWorkId)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
val response = OriginalWorkResponse.from(originalWork)
|
||||||
|
assertEquals(listOf("https://original.test/admin-detail"), response.originalLinks)
|
||||||
|
assertEquals(listOf("admin-detail-tag"), response.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자 원작 목록 조회 결과는 트랜잭션 밖에서도 DTO로 변환할 수 있다")
|
||||||
|
fun shouldLoadAdminOriginalWorkPageRelationsOutsideTransaction() {
|
||||||
|
saveOriginalWorkWithLinkAndTag("admin-page")
|
||||||
|
|
||||||
|
val page = transactionTemplate.execute {
|
||||||
|
adminOriginalWorkService.getOriginalWorkPage(page = 0, size = 20)
|
||||||
|
}!!
|
||||||
|
|
||||||
|
val response = OriginalWorkResponse.from(page.content.single())
|
||||||
|
assertEquals(listOf("https://original.test/admin-page"), response.originalLinks)
|
||||||
|
assertEquals(listOf("admin-page-tag"), response.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자 원작 검색 결과는 트랜잭션 밖에서도 DTO로 변환할 수 있다")
|
||||||
|
fun shouldLoadAdminOriginalWorkSearchRelationsOutsideTransaction() {
|
||||||
|
saveOriginalWorkWithLinkAndTag("admin-search")
|
||||||
|
|
||||||
|
val originalWorks = transactionTemplate.execute {
|
||||||
|
adminOriginalWorkService.searchOriginalWorksAll("admin-search-title")
|
||||||
|
}!!
|
||||||
|
|
||||||
|
val response = OriginalWorkResponse.from(originalWorks.single())
|
||||||
|
assertEquals(listOf("https://original.test/admin-search"), response.originalLinks)
|
||||||
|
assertEquals(listOf("admin-search-tag"), response.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("캐릭터 번역 job 스케줄러는 트랜잭션 밖 호출에서도 lazy 관계를 추출할 수 있다")
|
||||||
|
fun shouldScheduleCharacterTranslationOutsideTransaction() {
|
||||||
|
val characterId = saveCharacterForTranslation("translation")
|
||||||
|
|
||||||
|
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||||
|
resourceType = LanguageTranslationTargetType.CHARACTER,
|
||||||
|
resourceId = characterId,
|
||||||
|
targetLanguage = "en"
|
||||||
|
)
|
||||||
|
|
||||||
|
val jobs = translationJobRepository.findAll()
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"gender",
|
||||||
|
"personalityTrait",
|
||||||
|
"personalityDescription",
|
||||||
|
"backgroundTopic",
|
||||||
|
"backgroundDescription",
|
||||||
|
"tags"
|
||||||
|
),
|
||||||
|
jobs.map { it.fieldKey }.sortedBy { expectedCharacterTranslationFieldOrder().indexOf(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("크리에이터 랭킹 조회 결과는 트랜잭션 밖에서도 explorer creator DTO로 변환할 수 있다")
|
||||||
|
fun shouldLoadCreatorRankingTagsOutsideTransaction() {
|
||||||
|
saveCreatorRankingWithTag()
|
||||||
|
|
||||||
|
val creators = rankingRepository.getCreatorRankings(memberId = null)
|
||||||
|
|
||||||
|
val response = creators.single().toExplorerSectionCreator("https://cdn.test")
|
||||||
|
assertEquals("#creator-tag", response.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCharacterWithTag(seed: String): Long {
|
||||||
|
return transactionTemplate.execute {
|
||||||
|
val tag = kr.co.vividnext.sodalive.chat.character.ChatCharacterTag("$seed-tag")
|
||||||
|
entityManager.persist(tag)
|
||||||
|
val character = ChatCharacter(
|
||||||
|
characterUUID = "$seed-character-uuid",
|
||||||
|
name = "$seed-character",
|
||||||
|
description = "$seed-description",
|
||||||
|
systemPrompt = "$seed-system-prompt"
|
||||||
|
)
|
||||||
|
character.creatorMember = persistCreator("$seed-character-creator")
|
||||||
|
character.addTag(tag)
|
||||||
|
chatCharacterRepository.save(character).id!!
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveTwoCharactersWithSharedTag(): Long {
|
||||||
|
return transactionTemplate.execute {
|
||||||
|
val tag = kr.co.vividnext.sodalive.chat.character.ChatCharacterTag("shared-tag")
|
||||||
|
entityManager.persist(tag)
|
||||||
|
|
||||||
|
val current = ChatCharacter(
|
||||||
|
characterUUID = "shared-current-uuid",
|
||||||
|
name = "shared-current",
|
||||||
|
description = "shared-current-description",
|
||||||
|
systemPrompt = "shared-current-system-prompt"
|
||||||
|
)
|
||||||
|
current.creatorMember = persistCreator("shared-current-creator")
|
||||||
|
current.addTag(tag)
|
||||||
|
chatCharacterRepository.save(current)
|
||||||
|
|
||||||
|
val other = ChatCharacter(
|
||||||
|
characterUUID = "shared-other-uuid",
|
||||||
|
name = "shared-other",
|
||||||
|
description = "shared-other-description",
|
||||||
|
systemPrompt = "shared-other-system-prompt"
|
||||||
|
)
|
||||||
|
other.creatorMember = persistCreator("shared-other-creator")
|
||||||
|
other.addTag(tag)
|
||||||
|
chatCharacterRepository.save(other)
|
||||||
|
|
||||||
|
current.id!!
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCharacterWithDetailMappings(seed: String): Long {
|
||||||
|
return transactionTemplate.execute {
|
||||||
|
val value = kr.co.vividnext.sodalive.chat.character.ChatCharacterValue("$seed-value")
|
||||||
|
val hobby = kr.co.vividnext.sodalive.chat.character.ChatCharacterHobby("$seed-hobby")
|
||||||
|
val goal = kr.co.vividnext.sodalive.chat.character.ChatCharacterGoal("$seed-goal")
|
||||||
|
entityManager.persist(value)
|
||||||
|
entityManager.persist(hobby)
|
||||||
|
entityManager.persist(goal)
|
||||||
|
|
||||||
|
val character = ChatCharacter(
|
||||||
|
characterUUID = "$seed-character-uuid",
|
||||||
|
name = "$seed-character",
|
||||||
|
description = "$seed-description",
|
||||||
|
systemPrompt = "$seed-system-prompt"
|
||||||
|
)
|
||||||
|
character.creatorMember = persistCreator("$seed-character-creator")
|
||||||
|
character.addValue(value)
|
||||||
|
character.addHobby(hobby)
|
||||||
|
character.addGoal(goal)
|
||||||
|
chatCharacterRepository.save(character).id!!
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCharacterForTranslation(seed: String): Long {
|
||||||
|
return transactionTemplate.execute {
|
||||||
|
val tag = kr.co.vividnext.sodalive.chat.character.ChatCharacterTag("$seed-tag")
|
||||||
|
entityManager.persist(tag)
|
||||||
|
val character = ChatCharacter(
|
||||||
|
characterUUID = "$seed-character-uuid",
|
||||||
|
name = "$seed-character",
|
||||||
|
description = "$seed-description",
|
||||||
|
languageCode = "ko",
|
||||||
|
systemPrompt = "$seed-system-prompt",
|
||||||
|
gender = "female"
|
||||||
|
)
|
||||||
|
character.creatorMember = persistCreator("$seed-character-creator")
|
||||||
|
character.addTag(tag)
|
||||||
|
character.addPersonality("$seed-trait", "$seed-personality-description")
|
||||||
|
character.addBackground("$seed-topic", "$seed-background-description")
|
||||||
|
chatCharacterRepository.save(character).id!!
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectedCharacterTranslationFieldOrder(): List<String> {
|
||||||
|
return listOf(
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"gender",
|
||||||
|
"personalityTrait",
|
||||||
|
"personalityDescription",
|
||||||
|
"backgroundTopic",
|
||||||
|
"backgroundDescription",
|
||||||
|
"tags"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCharactersWithDuplicateSharedTags(): Long {
|
||||||
|
return transactionTemplate.execute {
|
||||||
|
val tagA = kr.co.vividnext.sodalive.chat.character.ChatCharacterTag("duplicate-shared-a")
|
||||||
|
val tagB = kr.co.vividnext.sodalive.chat.character.ChatCharacterTag("duplicate-shared-b")
|
||||||
|
entityManager.persist(tagA)
|
||||||
|
entityManager.persist(tagB)
|
||||||
|
|
||||||
|
val current = ChatCharacter(
|
||||||
|
characterUUID = "duplicate-current-uuid",
|
||||||
|
name = "duplicate-current",
|
||||||
|
description = "duplicate-current-description",
|
||||||
|
systemPrompt = "duplicate-current-system-prompt"
|
||||||
|
)
|
||||||
|
current.creatorMember = persistCreator("duplicate-current-creator")
|
||||||
|
current.addTag(tagA)
|
||||||
|
current.addTag(tagB)
|
||||||
|
chatCharacterRepository.save(current)
|
||||||
|
|
||||||
|
val otherWithTwoSharedTags = ChatCharacter(
|
||||||
|
characterUUID = "duplicate-other-two-uuid",
|
||||||
|
name = "duplicate-other-two",
|
||||||
|
description = "duplicate-other-two-description",
|
||||||
|
systemPrompt = "duplicate-other-two-system-prompt"
|
||||||
|
)
|
||||||
|
otherWithTwoSharedTags.creatorMember = persistCreator("duplicate-other-two-creator")
|
||||||
|
otherWithTwoSharedTags.addTag(tagA)
|
||||||
|
otherWithTwoSharedTags.addTag(tagB)
|
||||||
|
chatCharacterRepository.save(otherWithTwoSharedTags)
|
||||||
|
|
||||||
|
val otherWithOneSharedTag = ChatCharacter(
|
||||||
|
characterUUID = "duplicate-other-one-uuid",
|
||||||
|
name = "duplicate-other-one",
|
||||||
|
description = "duplicate-other-one-description",
|
||||||
|
systemPrompt = "duplicate-other-one-system-prompt"
|
||||||
|
)
|
||||||
|
otherWithOneSharedTag.creatorMember = persistCreator("duplicate-other-one-creator")
|
||||||
|
otherWithOneSharedTag.addTag(tagA)
|
||||||
|
chatCharacterRepository.save(otherWithOneSharedTag)
|
||||||
|
|
||||||
|
current.id!!
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveOriginalWorkWithLinkAndTag(seed: String = "original"): Long {
|
||||||
|
return transactionTemplate.execute {
|
||||||
|
val originalWork = OriginalWork(
|
||||||
|
title = "$seed-title",
|
||||||
|
contentType = "webtoon",
|
||||||
|
category = "romance",
|
||||||
|
description = "$seed-description"
|
||||||
|
)
|
||||||
|
val link = OriginalWorkLink(
|
||||||
|
url = "https://original.test/$seed",
|
||||||
|
originalWork = originalWork
|
||||||
|
)
|
||||||
|
val tag = OriginalWorkTag("$seed-tag")
|
||||||
|
val tagMapping = OriginalWorkTagMapping(originalWork, tag)
|
||||||
|
|
||||||
|
entityManager.persist(tag)
|
||||||
|
originalWork.originalLinks.add(link)
|
||||||
|
originalWork.tagMappings.add(tagMapping)
|
||||||
|
|
||||||
|
originalWorkRepository.save(originalWork).id!!
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persistCreator(seed: String): Member {
|
||||||
|
val creator = Member(
|
||||||
|
email = "$seed@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = seed,
|
||||||
|
role = MemberRole.CREATOR
|
||||||
|
)
|
||||||
|
entityManager.persist(creator)
|
||||||
|
return creator
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCreatorRankingWithTag() {
|
||||||
|
transactionTemplate.executeWithoutResult {
|
||||||
|
val creator = persistCreator("creator-osiv")
|
||||||
|
|
||||||
|
val tag = CreatorTag(tag = "creator-tag")
|
||||||
|
entityManager.persist(tag)
|
||||||
|
entityManager.persist(MemberCreatorTag(member = creator, tag = tag))
|
||||||
|
|
||||||
|
val ranking = CreatorRanking(ranking = 1)
|
||||||
|
ranking.member = creator
|
||||||
|
entityManager.persist(ranking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user