캐릭터 챗봇 #338
| @@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| import org.springframework.web.bind.annotation.RequestBody | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| @@ -42,4 +43,21 @@ class ChatRoomController( | ||||
|         val response = chatRoomService.createOrGetChatRoom(member, request.characterId) | ||||
|         ApiResponse.ok(response) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 내가 참여 중인 채팅방 목록 조회 API | ||||
|      * - 페이징(기본 20개) | ||||
|      * - 가장 최근 메시지 기준 내림차순 | ||||
|      */ | ||||
|     @GetMapping("/list") | ||||
|     fun listMyChatRooms( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null || member.auth == null) { | ||||
|             ApiResponse.ok(emptyList()) | ||||
|         } else { | ||||
|             val response = chatRoomService.listMyChatRooms(member) | ||||
|             ApiResponse.ok(response) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,25 @@ data class CreateChatRoomResponse( | ||||
|     val chatRoomId: Long | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * 채팅방 목록 아이템 DTO (API 응답용) | ||||
|  */ | ||||
| data class ChatRoomListItemDto( | ||||
|     val chatRoomId: Long, | ||||
|     val title: String, | ||||
|     val imageUrl: String, | ||||
|     val lastMessagePreview: String? | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * 채팅방 목록 쿼리 DTO (레포지토리 투영용) | ||||
|  */ | ||||
| data class ChatRoomListQueryDto( | ||||
|     val chatRoomId: Long, | ||||
|     val title: String, | ||||
|     val imagePath: String? | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * 외부 API 채팅 세션 응답 DTO | ||||
|  */ | ||||
|   | ||||
| @@ -0,0 +1,11 @@ | ||||
| package kr.co.vividnext.sodalive.chat.room.repository | ||||
|  | ||||
| import kr.co.vividnext.sodalive.chat.room.CharacterChatMessage | ||||
| import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
| import org.springframework.stereotype.Repository | ||||
|  | ||||
| @Repository | ||||
| interface CharacterChatMessageRepository : JpaRepository<CharacterChatMessage, Long> { | ||||
|     fun findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(chatRoom: CharacterChatRoom): CharacterChatMessage? | ||||
| } | ||||
| @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.chat.room.repository | ||||
|  | ||||
| import kr.co.vividnext.sodalive.chat.character.ChatCharacter | ||||
| import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom | ||||
| import kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
| import org.springframework.data.jpa.repository.Query | ||||
| @@ -13,10 +14,6 @@ interface CharacterChatRoomRepository : JpaRepository<CharacterChatRoom, Long> { | ||||
|  | ||||
|     /** | ||||
|      * 특정 멤버와 캐릭터가 참여 중인 활성화된 채팅방을 찾는 쿼리 | ||||
|      * | ||||
|      * @param member 멤버 | ||||
|      * @param character 캐릭터 | ||||
|      * @return 활성화된 채팅방 (없으면 null) | ||||
|      */ | ||||
|     @Query( | ||||
|         """ | ||||
| @@ -32,4 +29,32 @@ interface CharacterChatRoomRepository : JpaRepository<CharacterChatRoom, Long> { | ||||
|         @Param("member") member: Member, | ||||
|         @Param("character") character: ChatCharacter | ||||
|     ): CharacterChatRoom? | ||||
|  | ||||
|     /** | ||||
|      * 멤버가 참여 중인 채팅방을 최근 메시지 시간 순으로 페이징 조회 | ||||
|      * - 메시지가 없으면 방 생성 시간(createdAt)으로 대체 | ||||
|      */ | ||||
|     @Query( | ||||
|         value = """ | ||||
|             SELECT new kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto( | ||||
|                 r.id, | ||||
|                 r.title, | ||||
|                 pc.character.imagePath | ||||
|             ) | ||||
|             FROM CharacterChatRoom r | ||||
|             JOIN r.participants p | ||||
|             JOIN r.participants pc | ||||
|             LEFT JOIN r.messages m | ||||
|             WHERE p.member = :member | ||||
|               AND p.isActive = true | ||||
|               AND pc.participantType = kr.co.vividnext.sodalive.chat.room.ParticipantType.CHARACTER | ||||
|               AND pc.isActive = true | ||||
|               AND r.isActive = true | ||||
|             GROUP BY r.id, r.title, r.createdAt, pc.character.imagePath | ||||
|             ORDER BY COALESCE(MAX(m.createdAt), r.createdAt) DESC | ||||
|         """ | ||||
|     ) | ||||
|     fun findMemberRoomsOrderByLastMessageDesc( | ||||
|         @Param("member") member: Member | ||||
|     ): List<ChatRoomListQueryDto> | ||||
| } | ||||
|   | ||||
| @@ -5,8 +5,11 @@ import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService | ||||
| import kr.co.vividnext.sodalive.chat.room.CharacterChatParticipant | ||||
| import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom | ||||
| import kr.co.vividnext.sodalive.chat.room.ParticipantType | ||||
| import kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListItemDto | ||||
| import kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto | ||||
| import kr.co.vividnext.sodalive.chat.room.dto.CreateChatRoomResponse | ||||
| import kr.co.vividnext.sodalive.chat.room.dto.ExternalChatSessionResponse | ||||
| import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatMessageRepository | ||||
| import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatParticipantRepository | ||||
| import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatRoomRepository | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| @@ -26,6 +29,7 @@ import java.util.UUID | ||||
| class ChatRoomService( | ||||
|     private val chatRoomRepository: CharacterChatRoomRepository, | ||||
|     private val participantRepository: CharacterChatParticipantRepository, | ||||
|     private val messageRepository: CharacterChatMessageRepository, | ||||
|     private val characterService: ChatCharacterService, | ||||
|  | ||||
|     @Value("\${weraser.api-key}") | ||||
| @@ -35,7 +39,10 @@ class ChatRoomService( | ||||
|     private val apiUrl: String, | ||||
|  | ||||
|     @Value("\${server.env}") | ||||
|     private val serverEnv: String | ||||
|     private val serverEnv: String, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
| @@ -164,4 +171,28 @@ class ChatRoomService( | ||||
|             throw SodaException("${e.message}, 채팅방 생성에 실패했습니다. 다시 시도해 주세요.") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Transactional(readOnly = true) | ||||
|     fun listMyChatRooms(member: Member): List<ChatRoomListItemDto> { | ||||
|         val rooms: List<ChatRoomListQueryDto> = chatRoomRepository.findMemberRoomsOrderByLastMessageDesc(member) | ||||
|         return rooms.map { q -> | ||||
|             val room = CharacterChatRoom( | ||||
|                 sessionId = "", | ||||
|                 title = q.title, | ||||
|                 isActive = true | ||||
|             ).apply { id = q.chatRoomId } | ||||
|  | ||||
|             val latest = messageRepository.findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(room) | ||||
|             val preview = latest?.message?.let { msg -> | ||||
|                 if (msg.length <= 25) msg else msg.substring(0, 25) + "..." | ||||
|             } | ||||
|  | ||||
|             ChatRoomListItemDto( | ||||
|                 chatRoomId = q.chatRoomId, | ||||
|                 title = q.title, | ||||
|                 imageUrl = "$imageHost/${q.imagePath ?: "profile/default-profile.png"}", | ||||
|                 lastMessagePreview = preview | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user