feat(chat): 채팅방 메시지 조회 API 구현

This commit is contained in:
Klaus 2025-08-08 16:00:30 +09:00
parent 1509ee0729
commit 002f2c2834
4 changed files with 69 additions and 0 deletions

View File

@ -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
* - 채팅방 참여 여부 검증

View File

@ -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 (레포지토리 투영용)
*/

View File

@ -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>
}

View File

@ -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
)
}
}
}