캐릭터 챗봇 #338
| @@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse | |||||||
| import kr.co.vividnext.sodalive.common.SodaException | import kr.co.vividnext.sodalive.common.SodaException | ||||||
| import kr.co.vividnext.sodalive.member.Member | import kr.co.vividnext.sodalive.member.Member | ||||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | 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.PostMapping | ||||||
| import org.springframework.web.bind.annotation.RequestBody | import org.springframework.web.bind.annotation.RequestBody | ||||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||||
| @@ -42,4 +43,21 @@ class ChatRoomController( | |||||||
|         val response = chatRoomService.createOrGetChatRoom(member, request.characterId) |         val response = chatRoomService.createOrGetChatRoom(member, request.characterId) | ||||||
|         ApiResponse.ok(response) |         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 |     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 |  * 외부 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.character.ChatCharacter | ||||||
| import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom | 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 kr.co.vividnext.sodalive.member.Member | ||||||
| import org.springframework.data.jpa.repository.JpaRepository | import org.springframework.data.jpa.repository.JpaRepository | ||||||
| import org.springframework.data.jpa.repository.Query | import org.springframework.data.jpa.repository.Query | ||||||
| @@ -13,10 +14,6 @@ interface CharacterChatRoomRepository : JpaRepository<CharacterChatRoom, Long> { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 특정 멤버와 캐릭터가 참여 중인 활성화된 채팅방을 찾는 쿼리 |      * 특정 멤버와 캐릭터가 참여 중인 활성화된 채팅방을 찾는 쿼리 | ||||||
|      * |  | ||||||
|      * @param member 멤버 |  | ||||||
|      * @param character 캐릭터 |  | ||||||
|      * @return 활성화된 채팅방 (없으면 null) |  | ||||||
|      */ |      */ | ||||||
|     @Query( |     @Query( | ||||||
|         """ |         """ | ||||||
| @@ -32,4 +29,32 @@ interface CharacterChatRoomRepository : JpaRepository<CharacterChatRoom, Long> { | |||||||
|         @Param("member") member: Member, |         @Param("member") member: Member, | ||||||
|         @Param("character") character: ChatCharacter |         @Param("character") character: ChatCharacter | ||||||
|     ): CharacterChatRoom? |     ): 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.CharacterChatParticipant | ||||||
| import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom | import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom | ||||||
| import kr.co.vividnext.sodalive.chat.room.ParticipantType | 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.CreateChatRoomResponse | ||||||
| import kr.co.vividnext.sodalive.chat.room.dto.ExternalChatSessionResponse | 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.CharacterChatParticipantRepository | ||||||
| import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatRoomRepository | import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatRoomRepository | ||||||
| import kr.co.vividnext.sodalive.common.SodaException | import kr.co.vividnext.sodalive.common.SodaException | ||||||
| @@ -26,6 +29,7 @@ import java.util.UUID | |||||||
| class ChatRoomService( | class ChatRoomService( | ||||||
|     private val chatRoomRepository: CharacterChatRoomRepository, |     private val chatRoomRepository: CharacterChatRoomRepository, | ||||||
|     private val participantRepository: CharacterChatParticipantRepository, |     private val participantRepository: CharacterChatParticipantRepository, | ||||||
|  |     private val messageRepository: CharacterChatMessageRepository, | ||||||
|     private val characterService: ChatCharacterService, |     private val characterService: ChatCharacterService, | ||||||
|  |  | ||||||
|     @Value("\${weraser.api-key}") |     @Value("\${weraser.api-key}") | ||||||
| @@ -35,7 +39,10 @@ class ChatRoomService( | |||||||
|     private val apiUrl: String, |     private val apiUrl: String, | ||||||
|  |  | ||||||
|     @Value("\${server.env}") |     @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}, 채팅방 생성에 실패했습니다. 다시 시도해 주세요.") |             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