feat(chat-room): 채팅방 리스트 응답 개선(타입/미리보기/상대 이미지/시간)
- ChatRoomListQueryDto: characterType, lastActivityAt 필드 추가 - ChatRoomListItemDto: opponentType, lastMessagePreview, lastMessageTimeLabel 제공 - 레포지토리 정렬 기준을 최근 메시지 또는 생성일로 일원화(COALESCE)
This commit is contained in:
		| @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.chat.room.dto | |||||||
|  |  | ||||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties | import com.fasterxml.jackson.annotation.JsonIgnoreProperties | ||||||
| import com.fasterxml.jackson.annotation.JsonProperty | import com.fasterxml.jackson.annotation.JsonProperty | ||||||
|  | import kr.co.vividnext.sodalive.chat.character.CharacterType | ||||||
|  | import java.time.LocalDateTime | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 채팅방 생성 요청 DTO |  * 채팅방 생성 요청 DTO | ||||||
| @@ -24,7 +26,9 @@ data class ChatRoomListItemDto( | |||||||
|     val chatRoomId: Long, |     val chatRoomId: Long, | ||||||
|     val title: String, |     val title: String, | ||||||
|     val imageUrl: String, |     val imageUrl: String, | ||||||
|     val lastMessagePreview: String? |     val opponentType: String, | ||||||
|  |     val lastMessagePreview: String?, | ||||||
|  |     val lastMessageTimeLabel: String | ||||||
| ) | ) | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -40,10 +44,13 @@ data class ChatMessageItemDto( | |||||||
| /** | /** | ||||||
|  * 채팅방 목록 쿼리 DTO (레포지토리 투영용) |  * 채팅방 목록 쿼리 DTO (레포지토리 투영용) | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| data class ChatRoomListQueryDto( | data class ChatRoomListQueryDto( | ||||||
|     val chatRoomId: Long, |     val chatRoomId: Long, | ||||||
|     val title: String, |     val title: String, | ||||||
|     val imagePath: String? |     val imagePath: String?, | ||||||
|  |     val characterType: CharacterType, | ||||||
|  |     val lastActivityAt: LocalDateTime? | ||||||
| ) | ) | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -40,7 +40,9 @@ interface ChatRoomRepository : JpaRepository<ChatRoom, Long> { | |||||||
|             SELECT new kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto( |             SELECT new kr.co.vividnext.sodalive.chat.room.dto.ChatRoomListQueryDto( | ||||||
|                 r.id, |                 r.id, | ||||||
|                 r.title, |                 r.title, | ||||||
|                 pc.character.imagePath |                 pc.character.imagePath, | ||||||
|  |                 pc.character.characterType, | ||||||
|  |                 COALESCE(MAX(m.createdAt), r.createdAt) | ||||||
|             ) |             ) | ||||||
|             FROM ChatRoom r |             FROM ChatRoom r | ||||||
|             JOIN r.participants p |             JOIN r.participants p | ||||||
|   | |||||||
| @@ -199,18 +199,39 @@ class ChatRoomService( | |||||||
|  |  | ||||||
|             val latest = messageRepository.findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(room) |             val latest = messageRepository.findTopByChatRoomAndIsActiveTrueOrderByCreatedAtDesc(room) | ||||||
|             val preview = latest?.message?.let { msg -> |             val preview = latest?.message?.let { msg -> | ||||||
|                 if (msg.length <= 25) msg else msg.take(25) + "..." |                 if (msg.length <= 30) msg else msg.take(30) + "..." | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             val imageUrl = "$imageHost/${q.imagePath ?: "profile/default-profile.png"}" | ||||||
|  |             val opponentType = q.characterType.name // Clone or Character | ||||||
|  |             val time = latest?.createdAt ?: q.lastActivityAt | ||||||
|  |             val timeLabel = formatRelativeTime(time) | ||||||
|  |  | ||||||
|             ChatRoomListItemDto( |             ChatRoomListItemDto( | ||||||
|                 chatRoomId = q.chatRoomId, |                 chatRoomId = q.chatRoomId, | ||||||
|                 title = q.title, |                 title = q.title, | ||||||
|                 imageUrl = "$imageHost/${q.imagePath ?: "profile/default-profile.png"}", |                 imageUrl = imageUrl, | ||||||
|                 lastMessagePreview = preview |                 opponentType = opponentType, | ||||||
|  |                 lastMessagePreview = preview, | ||||||
|  |                 lastMessageTimeLabel = timeLabel | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private fun formatRelativeTime(time: java.time.LocalDateTime?): String { | ||||||
|  |         if (time == null) return "" | ||||||
|  |         val now = java.time.LocalDateTime.now() | ||||||
|  |         val duration = java.time.Duration.between(time, now) | ||||||
|  |         val seconds = duration.seconds | ||||||
|  |         if (seconds <= 60) return "방금" | ||||||
|  |         val minutes = duration.toMinutes() | ||||||
|  |         if (minutes < 60) return "${'$'}minutes분 전" | ||||||
|  |         val hours = duration.toHours() | ||||||
|  |         if (hours < 24) return "${'$'}hours시간 전" | ||||||
|  |         // 그 외: 날짜 (yyyy-MM-dd) | ||||||
|  |         return time.toLocalDate().toString() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     fun isMyRoomSessionActive(member: Member, chatRoomId: Long): Boolean { |     fun isMyRoomSessionActive(member: Member, chatRoomId: Long): Boolean { | ||||||
|         val room = chatRoomRepository.findById(chatRoomId).orElseThrow { |         val room = chatRoomRepository.findById(chatRoomId).orElseThrow { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user