From df77e310431299325c81b69467580c43d018a8ed Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 14 Aug 2025 21:56:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=9E=85=EC=9E=A5=20API=EC=99=80=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=B4=88=EA=B8=B0=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/chat/room/{chatRoomId}/enter 엔드포인트 추가 - 참여 검증 후 roomId, character(아이디/이름/프로필/타입) 제공 - 최신 20개 메시지 조회(내림차순 조회 후 createdAt 오름차순으로 정렬) - hasMoreMessages 플래그 계산(가장 오래된 메시지 이전 존재 여부 판단) --- .../room/controller/ChatRoomController.kt | 17 +++++ .../sodalive/chat/room/dto/ChatRoomDto.kt | 17 +++++ .../chat/room/service/ChatRoomService.kt | 66 +++++++++++++++++++ 3 files changed, 100 insertions(+) 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()