feat(chat-room): 채팅방 목록 API 응답 구조 개편 및 최근 메시지/프로필 이미지 제공\n\n- 페이징 객체 제거: ApiResponse<List<ChatRoomListItemDto>> 형태로 반환\n- 메시지 보낸 시간 필드 제거\n- 상대방(캐릭터) 프로필 이미지 URL 제공 (imageHost/imagePath 조합 -> imageUrl)\n- 가장 최근 메시지 1개 미리보기 제공 (최대 25자, 초과 시 ... 처리)\n- 목록 조회 쿼리 투영 DTO 및 정렬 로직 개선 (최근 메시지 없으면 방 생성 시간 사용)\n- 비인증/미본인인증 사용자: 빈 리스트 반환
This commit is contained in:
parent
1bafbed17c
commit
4d1f84cc5c
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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<CharacterChatMessage, Long> {
|
||||
fun findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(chatRoom: CharacterChatRoom): CharacterChatMessage?
|
||||
}
|
|
@ -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<CharacterChatRoom, Long> {
|
|||
|
||||
/**
|
||||
* 특정 멤버와 캐릭터가 참여 중인 활성화된 채팅방을 찾는 쿼리
|
||||
*
|
||||
* @param member 멤버
|
||||
* @param character 캐릭터
|
||||
* @return 활성화된 채팅방 (없으면 null)
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
|
@ -32,4 +29,32 @@ interface CharacterChatRoomRepository : JpaRepository<CharacterChatRoom, Long> {
|
|||
@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<ChatRoomListQueryDto>
|
||||
}
|
||||
|
|
|
@ -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<ChatRoomListItemDto> {
|
||||
val rooms: List<ChatRoomListQueryDto> = 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue