캐릭터 챗봇 #338
| @@ -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 | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  | ) | ||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 태그를 찾거나 생성하여 캐릭터에 연결 |      * 태그를 찾거나 생성하여 캐릭터에 연결 | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -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() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user