캐릭터 챗봇 #338
| @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.PathVariable | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| import org.springframework.web.bind.annotation.RequestBody | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @@ -62,6 +63,24 @@ class ChatRoomController( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 채팅방 메시지 조회 API | ||||
|      * - 참여 여부 검증(미참여시 "잘못된 접근입니다") | ||||
|      * - messageId가 있으면 해당 ID 이전 20개, 없으면 최신 20개 | ||||
|      */ | ||||
|     @GetMapping("/{chatRoomId}/messages") | ||||
|     fun getChatMessages( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, | ||||
|         @PathVariable chatRoomId: Long, | ||||
|         @RequestParam(required = false) messageId: Long? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|         if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") | ||||
|  | ||||
|         val response = chatRoomService.getChatMessages(member, chatRoomId, messageId) | ||||
|         ApiResponse.ok(response) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 세션 상태 조회 API | ||||
|      * - 채팅방 참여 여부 검증 | ||||
|   | ||||
| @@ -24,6 +24,16 @@ data class ChatRoomListItemDto( | ||||
|     val lastMessagePreview: String? | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * 채팅방 메시지 아이템 DTO (API 응답용) | ||||
|  */ | ||||
| data class ChatMessageItemDto( | ||||
|     val messageId: Long, | ||||
|     val message: String, | ||||
|     val profileImageUrl: String, | ||||
|     val mine: Boolean | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * 채팅방 목록 쿼리 DTO (레포지토리 투영용) | ||||
|  */ | ||||
|   | ||||
| @@ -8,4 +8,11 @@ import org.springframework.stereotype.Repository | ||||
| @Repository | ||||
| interface CharacterChatMessageRepository : JpaRepository<CharacterChatMessage, Long> { | ||||
|     fun findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(chatRoom: CharacterChatRoom): CharacterChatMessage? | ||||
|  | ||||
|     fun findTop20ByChatRoomAndIsActiveTrueOrderByIdDesc(chatRoom: CharacterChatRoom): List<CharacterChatMessage> | ||||
|  | ||||
|     fun findTop20ByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc( | ||||
|         chatRoom: CharacterChatRoom, | ||||
|         id: Long | ||||
|     ): List<CharacterChatMessage> | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ 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.ChatMessageItemDto | ||||
| 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 | ||||
| @@ -324,4 +325,36 @@ class ChatRoomService( | ||||
|         // 최종 실패 로그 (예외 미전파) | ||||
|         log.error("[chat] 외부 세션 종료 최종 실패: sessionId={}, attempts={}", sessionId, maxAttempts) | ||||
|     } | ||||
|  | ||||
|     @Transactional(readOnly = true) | ||||
|     fun getChatMessages(member: Member, chatRoomId: Long, beforeMessageId: Long?): List<ChatMessageItemDto> { | ||||
|         val room = chatRoomRepository.findById(chatRoomId).orElseThrow { | ||||
|             SodaException("채팅방을 찾을 수 없습니다.") | ||||
|         } | ||||
|         val participant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) | ||||
|         if (participant == null) { | ||||
|             throw SodaException("잘못된 접근입니다") | ||||
|         } | ||||
|  | ||||
|         val messages = if (beforeMessageId != null) { | ||||
|             messageRepository.findTop20ByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc(room, beforeMessageId) | ||||
|         } else { | ||||
|             messageRepository.findTop20ByChatRoomAndIsActiveTrueOrderByIdDesc(room) | ||||
|         } | ||||
|  | ||||
|         return messages.map { msg -> | ||||
|             val sender = msg.participant | ||||
|             val profilePath = when (sender.participantType) { | ||||
|                 ParticipantType.USER -> sender.member?.profileImage | ||||
|                 ParticipantType.CHARACTER -> sender.character?.imagePath | ||||
|             } | ||||
|             val imageUrl = "$imageHost/${profilePath ?: "profile/default-profile.png"}" | ||||
|             ChatMessageItemDto( | ||||
|                 messageId = msg.id!!, | ||||
|                 message = msg.message, | ||||
|                 profileImageUrl = imageUrl, | ||||
|                 mine = sender.member?.id == member.id | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user