diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt index 1a02229..7be88a4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt @@ -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 * - 채팅방 참여 여부 검증 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/dto/ChatRoomDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/dto/ChatRoomDto.kt index 5768d7e..60ffe61 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/dto/ChatRoomDto.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/dto/ChatRoomDto.kt @@ -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 (레포지토리 투영용) */ diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatMessageRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatMessageRepository.kt index a950cac..10a4f2f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatMessageRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatMessageRepository.kt @@ -8,4 +8,11 @@ import org.springframework.stereotype.Repository @Repository interface CharacterChatMessageRepository : JpaRepository { fun findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(chatRoom: CharacterChatRoom): CharacterChatMessage? + + fun findTop20ByChatRoomAndIsActiveTrueOrderByIdDesc(chatRoom: CharacterChatRoom): List + + fun findTop20ByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc( + chatRoom: CharacterChatRoom, + id: Long + ): List } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt index d887859..2a7878c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt @@ -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 { + 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 + ) + } + } }