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 e595c83..20c6892 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 @@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -42,4 +43,21 @@ class ChatRoomController( val response = chatRoomService.createOrGetChatRoom(member, request.characterId) ApiResponse.ok(response) } + + /** + * 내가 참여 중인 채팅방 목록 조회 API + * - 페이징(기본 20개) + * - 가장 최근 메시지 기준 내림차순 + */ + @GetMapping("/list") + fun listMyChatRooms( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null || member.auth == null) { + ApiResponse.ok(emptyList()) + } else { + val response = chatRoomService.listMyChatRooms(member) + ApiResponse.ok(response) + } + } } 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 2b53285..030c52a 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 @@ -14,6 +14,25 @@ data class CreateChatRoomResponse( val chatRoomId: Long ) +/** + * 채팅방 목록 아이템 DTO (API 응답용) + */ +data class ChatRoomListItemDto( + val chatRoomId: Long, + val title: String, + val imageUrl: String, + val lastMessagePreview: String? +) + +/** + * 채팅방 목록 쿼리 DTO (레포지토리 투영용) + */ +data class ChatRoomListQueryDto( + val chatRoomId: Long, + val title: String, + val imagePath: String? +) + /** * 외부 API 채팅 세션 응답 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 new file mode 100644 index 0000000..a950cac --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatMessageRepository.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.chat.room.repository + +import kr.co.vividnext.sodalive.chat.room.CharacterChatMessage +import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface CharacterChatMessageRepository : JpaRepository { + fun findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(chatRoom: CharacterChatRoom): CharacterChatMessage? +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatRoomRepository.kt index 469fc46..84d56a7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/CharacterChatRoomRepository.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.chat.room.repository import kr.co.vividnext.sodalive.chat.character.ChatCharacter import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom +import kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto import kr.co.vividnext.sodalive.member.Member import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query @@ -13,10 +14,6 @@ interface CharacterChatRoomRepository : JpaRepository { /** * 특정 멤버와 캐릭터가 참여 중인 활성화된 채팅방을 찾는 쿼리 - * - * @param member 멤버 - * @param character 캐릭터 - * @return 활성화된 채팅방 (없으면 null) */ @Query( """ @@ -32,4 +29,32 @@ interface CharacterChatRoomRepository : JpaRepository { @Param("member") member: Member, @Param("character") character: ChatCharacter ): CharacterChatRoom? + + /** + * 멤버가 참여 중인 채팅방을 최근 메시지 시간 순으로 페이징 조회 + * - 메시지가 없으면 방 생성 시간(createdAt)으로 대체 + */ + @Query( + value = """ + SELECT new kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto( + r.id, + r.title, + pc.character.imagePath + ) + FROM CharacterChatRoom r + JOIN r.participants p + JOIN r.participants pc + LEFT JOIN r.messages m + WHERE p.member = :member + AND p.isActive = true + AND pc.participantType = kr.co.vividnext.sodalive.chat.room.ParticipantType.CHARACTER + AND pc.isActive = true + AND r.isActive = true + GROUP BY r.id, r.title, r.createdAt, pc.character.imagePath + ORDER BY COALESCE(MAX(m.createdAt), r.createdAt) DESC + """ + ) + fun findMemberRoomsOrderByLastMessageDesc( + @Param("member") member: Member + ): 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 4a8f47f..7983ebb 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,8 +5,11 @@ 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.ChatRoomListItemDto +import kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto import kr.co.vividnext.sodalive.chat.room.dto.CreateChatRoomResponse import kr.co.vividnext.sodalive.chat.room.dto.ExternalChatSessionResponse +import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatMessageRepository import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatParticipantRepository import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatRoomRepository import kr.co.vividnext.sodalive.common.SodaException @@ -26,6 +29,7 @@ import java.util.UUID class ChatRoomService( private val chatRoomRepository: CharacterChatRoomRepository, private val participantRepository: CharacterChatParticipantRepository, + private val messageRepository: CharacterChatMessageRepository, private val characterService: ChatCharacterService, @Value("\${weraser.api-key}") @@ -35,7 +39,10 @@ class ChatRoomService( private val apiUrl: String, @Value("\${server.env}") - private val serverEnv: String + private val serverEnv: String, + + @Value("\${cloud.aws.cloud-front.host}") + private val imageHost: String ) { /** @@ -164,4 +171,28 @@ class ChatRoomService( throw SodaException("${e.message}, 채팅방 생성에 실패했습니다. 다시 시도해 주세요.") } } + + @Transactional(readOnly = true) + fun listMyChatRooms(member: Member): List { + val rooms: List = chatRoomRepository.findMemberRoomsOrderByLastMessageDesc(member) + return rooms.map { q -> + val room = CharacterChatRoom( + sessionId = "", + title = q.title, + isActive = true + ).apply { id = q.chatRoomId } + + val latest = messageRepository.findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(room) + val preview = latest?.message?.let { msg -> + if (msg.length <= 25) msg else msg.substring(0, 25) + "..." + } + + ChatRoomListItemDto( + chatRoomId = q.chatRoomId, + title = q.title, + imageUrl = "$imageHost/${q.imagePath ?: "profile/default-profile.png"}", + lastMessagePreview = preview + ) + } + } }