feat(character): 캐릭터 메인 API 추가

This commit is contained in:
Klaus 2025-08-07 22:33:29 +09:00
parent b0a6fc6498
commit a1533c8e98
6 changed files with 155 additions and 2 deletions

View File

@ -0,0 +1,88 @@
package kr.co.vividnext.sodalive.chat.character.controller
import kr.co.vividnext.sodalive.chat.character.dto.Character
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBannerResponse
import kr.co.vividnext.sodalive.chat.character.dto.CharacterMainResponse
import kr.co.vividnext.sodalive.chat.character.dto.CurationSection
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.member.Member
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.PageRequest
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/chat/character")
class ChatCharacterController(
private val service: ChatCharacterService,
private val bannerService: ChatCharacterBannerService,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
@GetMapping("/main")
fun getCharacterMain(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
): ApiResponse<CharacterMainResponse> = run {
// 배너 조회 (최대 10개)
val banners = bannerService.getActiveBanners(PageRequest.of(0, 10))
.content
.map {
CharacterBannerResponse(
characterId = it.chatCharacter.id!!,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
// 최근 대화한 캐릭터 조회 (현재는 빈 리스트)
val recentCharacters = service.getRecentCharacters()
.map {
RecentCharacter(
characterId = it.id!!,
name = it.name,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
// 인기 캐릭터 조회 (현재는 빈 리스트)
val popularCharacters = service.getPopularCharacters()
.map {
Character(
characterId = it.id!!,
name = it.name,
description = it.description,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
// 최신 캐릭터 조회 (최대 10개)
val newCharacters = service.getNewCharacters(10)
.map {
Character(
characterId = it.id!!,
name = it.name,
description = it.description,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
// 큐레이션 섹션 (현재는 빈 리스트)
val curationSections = emptyList<CurationSection>()
// 응답 생성
ApiResponse.ok(
CharacterMainResponse(
banners = banners,
recentCharacters = recentCharacters,
popularCharacters = popularCharacters,
newCharacters = newCharacters,
curationSections = curationSections
)
)
}
}

View File

@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.chat.character.dto
data class CharacterMainResponse(
val banners: List<CharacterBannerResponse>,
val recentCharacters: List<RecentCharacter>,
val popularCharacters: List<Character>,
val newCharacters: List<Character>,
val curationSections: List<CurationSection>
)
data class CurationSection(
val characterCurationId: Long,
val title: String,
val characters: List<Character>
)
data class Character(
val characterId: Long,
val name: String,
val description: String,
val imageUrl: String
)
data class RecentCharacter(
val characterId: Long,
val name: String,
val imageUrl: String
)
data class CharacterBannerResponse(
val characterId: Long,
val imageUrl: String
)

View File

@ -10,7 +10,7 @@ import org.springframework.stereotype.Repository
@Repository @Repository
interface ChatCharacterBannerRepository : JpaRepository<ChatCharacterBanner, Long> { interface ChatCharacterBannerRepository : JpaRepository<ChatCharacterBanner, Long> {
// 활성화된 배너 목록 조회 (정렬 순서대로) // 활성화된 배너 목록 조회 (정렬 순서대로)
fun findByActiveTrueOrderBySortOrderAsc(pageable: Pageable): Page<ChatCharacterBanner> fun findByIsActiveTrueOrderBySortOrderAsc(pageable: Pageable): Page<ChatCharacterBanner>
// 활성화된 배너 중 최대 정렬 순서 값 조회 // 활성화된 배너 중 최대 정렬 순서 값 조회
@Query("SELECT MAX(b.sortOrder) FROM ChatCharacterBanner b WHERE b.isActive = true") @Query("SELECT MAX(b.sortOrder) FROM ChatCharacterBanner b WHERE b.isActive = true")

View File

@ -18,7 +18,7 @@ class ChatCharacterBannerService(
* 활성화된 모든 배너 조회 (정렬 순서대로) * 활성화된 모든 배너 조회 (정렬 순서대로)
*/ */
fun getActiveBanners(pageable: Pageable): Page<ChatCharacterBanner> { fun getActiveBanners(pageable: Pageable): Page<ChatCharacterBanner> {
return bannerRepository.findByActiveTrueOrderBySortOrderAsc(pageable) return bannerRepository.findByIsActiveTrueOrderBySortOrderAsc(pageable)
} }
/** /**

View File

@ -23,6 +23,37 @@ class ChatCharacterService(
private val goalRepository: ChatCharacterGoalRepository private val goalRepository: ChatCharacterGoalRepository
) { ) {
/**
* 최근에 대화한 캐릭터 목록 조회
* 현재는 채팅방 구현 전이므로 리스트 반환
*/
@Transactional(readOnly = true)
fun getRecentCharacters(): List<ChatCharacter> {
// 채팅방 구현 전이므로 빈 리스트 반환
return emptyList()
}
/**
* 일주일간 대화가 가장 많은 인기 캐릭터 목록 조회
* 현재는 채팅방 구현 전이므로 리스트 반환
*/
@Transactional(readOnly = true)
fun getPopularCharacters(): List<ChatCharacter> {
// 채팅방 구현 전이므로 빈 리스트 반환
return emptyList()
}
/**
* 최근 등록된 캐릭터 목록 조회 (최대 10)
*/
@Transactional(readOnly = true)
fun getNewCharacters(limit: Int = 10): List<ChatCharacter> {
return chatCharacterRepository.findAll()
.filter { it.isActive }
.sortedByDescending { it.createdAt }
.take(limit)
}
/** /**
* 태그를 찾거나 생성하여 캐릭터에 연결 * 태그를 찾거나 생성하여 캐릭터에 연결
*/ */

View File

@ -93,6 +93,7 @@ class SecurityConfig(
.antMatchers(HttpMethod.GET, "/live/recommend").permitAll() .antMatchers(HttpMethod.GET, "/live/recommend").permitAll()
.antMatchers("/ad-tracking/app-launch").permitAll() .antMatchers("/ad-tracking/app-launch").permitAll()
.antMatchers(HttpMethod.GET, "/notice/latest").permitAll() .antMatchers(HttpMethod.GET, "/notice/latest").permitAll()
.antMatchers(HttpMethod.GET, "/api/chat/character/main").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
.and() .and()
.build() .build()