test #340
| @@ -22,6 +22,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal | |||||||
| import org.springframework.web.bind.annotation.GetMapping | import org.springframework.web.bind.annotation.GetMapping | ||||||
| import org.springframework.web.bind.annotation.PathVariable | import org.springframework.web.bind.annotation.PathVariable | ||||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||||
|  | import org.springframework.web.bind.annotation.RequestParam | ||||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||||
|  |  | ||||||
| @RestController | @RestController | ||||||
| @@ -67,16 +68,11 @@ class ChatCharacterController( | |||||||
|         // 인기 캐릭터 조회 |         // 인기 캐릭터 조회 | ||||||
|         val popularCharacters = service.getPopularCharacters() |         val popularCharacters = service.getPopularCharacters() | ||||||
|  |  | ||||||
|         // 최신 캐릭터 조회 (최대 10개) |         // 최근 등록된 캐릭터 리스트 조회 | ||||||
|         val newCharacters = service.getNewCharacters(50) |         val newCharacters = service.getRecentCharacters( | ||||||
|             .map { |             page = 0, | ||||||
|                 Character( |             size = 50 | ||||||
|                     characterId = it.id!!, |         ) | ||||||
|                     name = it.name, |  | ||||||
|                     description = it.description, |  | ||||||
|                     imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}" |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|         // 큐레이션 섹션 (활성화된 큐레이션 + 캐릭터) |         // 큐레이션 섹션 (활성화된 큐레이션 + 캐릭터) | ||||||
|         val curationSections = curationQueryService.getActiveCurationsWithCharacters() |         val curationSections = curationQueryService.getActiveCurationsWithCharacters() | ||||||
| @@ -182,4 +178,19 @@ class ChatCharacterController( | |||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 최근 등록된 캐릭터 전체보기 | ||||||
|  |      * - 기준: 2주 이내 등록된 캐릭터만 페이징 조회 | ||||||
|  |      * - 예외: 2주 이내 캐릭터가 0개인 경우, 최근 등록한 캐릭터 20개만 제공 | ||||||
|  |      */ | ||||||
|  |     @GetMapping("/recent") | ||||||
|  |     fun getRecentCharacters(@RequestParam("page", required = false) page: Int?) = run { | ||||||
|  |         ApiResponse.ok( | ||||||
|  |             service.getRecentCharacters( | ||||||
|  |                 page = page ?: 0, | ||||||
|  |                 size = 20 | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,14 +10,25 @@ import org.springframework.stereotype.Repository | |||||||
|  |  | ||||||
| @Repository | @Repository | ||||||
| interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> { | interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> { | ||||||
|     fun findByCharacterUUID(characterUUID: String): ChatCharacter? |  | ||||||
|     fun findByName(name: String): ChatCharacter? |     fun findByName(name: String): ChatCharacter? | ||||||
|     fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacter> |     fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacter> | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 활성화된 캐릭터를 생성일 기준 내림차순으로 조회 |      * 2주 이내(파라미터 since 이상) 활성 캐릭터 페이징 조회 | ||||||
|      */ |      */ | ||||||
|     fun findByIsActiveTrueOrderByCreatedAtDesc(pageable: Pageable): List<ChatCharacter> |     @Query( | ||||||
|  |         """ | ||||||
|  |         SELECT c FROM ChatCharacter c | ||||||
|  |         WHERE c.isActive = true AND c.createdAt >= :since | ||||||
|  |         ORDER BY c.createdAt DESC | ||||||
|  |         """ | ||||||
|  |     ) | ||||||
|  |     fun findRecentSince(@Param("since") since: java.time.LocalDateTime, pageable: Pageable): Page<ChatCharacter> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 2주 이내(파라미터 since 이상) 활성 캐릭터 개수 | ||||||
|  |      */ | ||||||
|  |     fun countByIsActiveTrueAndCreatedAtGreaterThanEqual(since: java.time.LocalDateTime): Long | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 이름, 설명, MBTI, 태그로 캐릭터 검색 |      * 이름, 설명, MBTI, 태그로 캐릭터 검색 | ||||||
|   | |||||||
| @@ -20,8 +20,10 @@ import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterValueRepo | |||||||
| import org.springframework.beans.factory.annotation.Value | import org.springframework.beans.factory.annotation.Value | ||||||
| import org.springframework.cache.annotation.Cacheable | import org.springframework.cache.annotation.Cacheable | ||||||
| import org.springframework.data.domain.PageRequest | import org.springframework.data.domain.PageRequest | ||||||
|  | import org.springframework.data.domain.Sort | ||||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||||
|  | import java.time.LocalDateTime | ||||||
|  |  | ||||||
| @Service | @Service | ||||||
| class ChatCharacterService( | class ChatCharacterService( | ||||||
| @@ -66,11 +68,51 @@ class ChatCharacterService( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 최근 등록된 캐릭터 목록 조회 (최대 10개) |      * 최근 등록된 캐릭터 전체보기 (페이징) | ||||||
|  |      * - 기준: 현재 시각 기준 2주 이내 생성된 활성 캐릭터 | ||||||
|  |      * - 2주 이내 캐릭터가 0개라면: 최근 등록한 캐릭터 20개 반환(페이지 무시) | ||||||
|      */ |      */ | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     fun getNewCharacters(limit: Int = 10): List<ChatCharacter> { |     fun getRecentCharacters(page: Int = 0, size: Int = 20): List<Character> { | ||||||
|         return chatCharacterRepository.findByIsActiveTrueOrderByCreatedAtDesc(PageRequest.of(0, limit)) |         val safePage = if (page < 0) 0 else page | ||||||
|  |         val safeSize = when { | ||||||
|  |             size <= 0 -> 20 | ||||||
|  |             size > 50 -> 50 // 과도한 page size 방지 | ||||||
|  |             else -> size | ||||||
|  |         } | ||||||
|  |         val since = LocalDateTime.now().minusWeeks(2) | ||||||
|  |  | ||||||
|  |         val totalRecent = chatCharacterRepository.countByIsActiveTrueAndCreatedAtGreaterThanEqual(since) | ||||||
|  |         if (totalRecent == 0L) { | ||||||
|  |             if (safePage > 0) { | ||||||
|  |                 return emptyList() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             val fallback = chatCharacterRepository.findByIsActiveTrue( | ||||||
|  |                 PageRequest.of(0, 20, Sort.by("createdAt").descending()) | ||||||
|  |             ) | ||||||
|  |             return fallback.content.map { | ||||||
|  |                 Character( | ||||||
|  |                     characterId = it.id!!, | ||||||
|  |                     name = it.name, | ||||||
|  |                     description = it.description, | ||||||
|  |                     imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}" | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val pageResult = chatCharacterRepository.findRecentSince( | ||||||
|  |             since, | ||||||
|  |             PageRequest.of(safePage, safeSize) | ||||||
|  |         ) | ||||||
|  |         return pageResult.content.map { | ||||||
|  |             Character( | ||||||
|  |                 characterId = it.id!!, | ||||||
|  |                 name = it.name, | ||||||
|  |                 description = it.description, | ||||||
|  |                 imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user