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 948d6e2..0a6f2a0 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 @@ -82,6 +82,23 @@ class ChatRoomController( ApiResponse.ok(isActive) } + /** + * 채팅방 입장 API + * - 참여 여부 검증 + * - 최신 20개 메시지를 createdAt 오름차순으로 반환 + */ + @GetMapping("/{chatRoomId}/enter") + fun enterChatRoom( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + @PathVariable chatRoomId: Long + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") + + val response = chatRoomService.enterChatRoom(member, chatRoomId) + ApiResponse.ok(response) + } + /** * 채팅방 나가기 API * - URL에 chatRoomId 포함 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 48dd667..e5b26e3 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 @@ -161,3 +161,20 @@ data class ExternalCharacterMessage( @JsonProperty("timestamp") val timestamp: String, @JsonProperty("messageType") val messageType: String ) + +/** + * 채팅방 입장 응답 DTO + */ +data class ChatRoomEnterCharacterDto( + val characterId: Long, + val name: String, + val profileImageUrl: String, + val characterType: String +) + +data class ChatRoomEnterResponse( + val roomId: Long, + val character: ChatRoomEnterCharacterDto, + val messages: List, + val hasMoreMessages: Boolean +) 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 7ff7849..bb7049d 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 @@ -8,6 +8,8 @@ import kr.co.vividnext.sodalive.chat.room.ChatRoom 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.ChatMessagesPageResponse +import kr.co.vividnext.sodalive.chat.room.dto.ChatRoomEnterCharacterDto +import kr.co.vividnext.sodalive.chat.room.dto.ChatRoomEnterResponse 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 @@ -247,6 +249,70 @@ class ChatRoomService( return fetchSessionActive(room.sessionId) } + @Transactional(readOnly = true) + fun enterChatRoom(member: Member, chatRoomId: Long): ChatRoomEnterResponse { + val room = chatRoomRepository.findById(chatRoomId).orElseThrow { + SodaException("채팅방을 찾을 수 없습니다.") + } + // 참여 여부 검증 + participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) + ?: throw SodaException("잘못된 접근입니다") + + // 캐릭터 참여자 조회 + val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue( + room, + ParticipantType.CHARACTER + ) ?: throw SodaException("잘못된 접근입니다") + + val character = characterParticipant.character + ?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") + + val imageUrl = "$imageHost/${character.imagePath ?: "profile/default-profile.png"}" + val characterDto = ChatRoomEnterCharacterDto( + characterId = character.id!!, + name = character.name, + profileImageUrl = imageUrl, + characterType = character.characterType.name + ) + + // 메시지 최신 20개 조회 후 createdAt 오름차순으로 반환 + val pageable = PageRequest.of(0, 20) + val fetched = messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(room, pageable) + + val nextCursor: Long? = fetched.minByOrNull { it.id ?: Long.MAX_VALUE }?.id + val hasMore: Boolean = if (nextCursor != null) { + messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, nextCursor) + } else { + false + } + + val messagesAsc = fetched.sortedBy { it.createdAt } + val items = messagesAsc.map { msg -> + val sender = msg.participant + val profilePath = when (sender.participantType) { + ParticipantType.USER -> sender.member?.profileImage + ParticipantType.CHARACTER -> sender.character?.imagePath + } + val senderImageUrl = "$imageHost/${profilePath ?: "profile/default-profile.png"}" + val createdAtMillis = msg.createdAt?.atZone(java.time.ZoneId.systemDefault())?.toInstant()?.toEpochMilli() + ?: 0L + ChatMessageItemDto( + messageId = msg.id!!, + message = msg.message, + profileImageUrl = senderImageUrl, + mine = sender.member?.id == member.id, + createdAt = createdAtMillis + ) + } + + return ChatRoomEnterResponse( + roomId = room.id!!, + character = characterDto, + messages = items, + hasMoreMessages = hasMore + ) + } + private fun fetchSessionActive(sessionId: String): Boolean { try { val factory = SimpleClientHttpRequestFactory()