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.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
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.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
@ -42,4 +43,21 @@ class ChatRoomController(
|
||||||
val response = chatRoomService.createOrGetChatRoom(member, request.characterId)
|
val response = chatRoomService.createOrGetChatRoom(member, request.characterId)
|
||||||
ApiResponse.ok(response)
|
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
|
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
|
* 외부 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.character.ChatCharacter
|
||||||
import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom
|
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 kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.data.jpa.repository.Query
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
@ -13,10 +14,6 @@ interface CharacterChatRoomRepository : JpaRepository<CharacterChatRoom, Long> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 멤버와 캐릭터가 참여 중인 활성화된 채팅방을 찾는 쿼리
|
* 특정 멤버와 캐릭터가 참여 중인 활성화된 채팅방을 찾는 쿼리
|
||||||
*
|
|
||||||
* @param member 멤버
|
|
||||||
* @param character 캐릭터
|
|
||||||
* @return 활성화된 채팅방 (없으면 null)
|
|
||||||
*/
|
*/
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
@ -32,4 +29,32 @@ interface CharacterChatRoomRepository : JpaRepository<CharacterChatRoom, Long> {
|
||||||
@Param("member") member: Member,
|
@Param("member") member: Member,
|
||||||
@Param("character") character: ChatCharacter
|
@Param("character") character: ChatCharacter
|
||||||
): CharacterChatRoom?
|
): 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.CharacterChatParticipant
|
||||||
import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom
|
import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom
|
||||||
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
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.CreateChatRoomResponse
|
||||||
import kr.co.vividnext.sodalive.chat.room.dto.ExternalChatSessionResponse
|
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.CharacterChatParticipantRepository
|
||||||
import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatRoomRepository
|
import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatRoomRepository
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
@ -26,6 +29,7 @@ import java.util.UUID
|
||||||
class ChatRoomService(
|
class ChatRoomService(
|
||||||
private val chatRoomRepository: CharacterChatRoomRepository,
|
private val chatRoomRepository: CharacterChatRoomRepository,
|
||||||
private val participantRepository: CharacterChatParticipantRepository,
|
private val participantRepository: CharacterChatParticipantRepository,
|
||||||
|
private val messageRepository: CharacterChatMessageRepository,
|
||||||
private val characterService: ChatCharacterService,
|
private val characterService: ChatCharacterService,
|
||||||
|
|
||||||
@Value("\${weraser.api-key}")
|
@Value("\${weraser.api-key}")
|
||||||
|
@ -35,7 +39,10 @@ class ChatRoomService(
|
||||||
private val apiUrl: String,
|
private val apiUrl: String,
|
||||||
|
|
||||||
@Value("\${server.env}")
|
@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}, 채팅방 생성에 실패했습니다. 다시 시도해 주세요.")
|
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