diff --git a/docs/api-specification.yaml b/docs/api-specification.yaml index a5f4203..4f66bd7 100644 --- a/docs/api-specification.yaml +++ b/docs/api-specification.yaml @@ -182,7 +182,10 @@ components: description: 채팅방 고유 식별자 title: type: string - description: 채팅방 제목(Guide-User 조합) + description: 채팅방 내부 식별용 기본 제목 (Guide-사용자 ID 조합) + displayTitle: + type: string + description: 요청자 기준 상대 닉네임을 포함한 표현용 제목 (예: "홍길동님과의 채팅") guideId: type: integer format: int64 diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt index 5d51364..eaa0178 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt @@ -42,7 +42,8 @@ class ChatRoomController( ): ResponseEntity> { val authenticatedId = requesterId ?: throw AccessDeniedException("인증이 필요합니다.") val room = roomService.createOneToOneRoom(req.guideId, req.userId, authenticatedId) - return ResponseEntity.ok(ApiResponse(msg = "채팅방 시작", data = ChatRoomResponse.from(room))) + val responseData = roomService.toResponse(room, authenticatedId) + return ResponseEntity.ok(ApiResponse(msg = "채팅방 시작", data = responseData)) } @DeleteMapping("/{roomId}") @@ -61,7 +62,7 @@ class ChatRoomController( @AuthenticationPrincipal requesterId: Long?, ): ResponseEntity> { val authenticatedId = requesterId ?: throw AccessDeniedException("인증이 필요합니다.") - val room = roomService.get(roomId, authenticatedId) - return ResponseEntity.ok(ApiResponse(msg = "채팅방 조회", data = ChatRoomResponse.from(room))) + val responseData = roomService.getResponse(roomId, authenticatedId) + return ResponseEntity.ok(ApiResponse(msg = "채팅방 조회", data = responseData)) } } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/dto/ChatRoomResponse.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/dto/ChatRoomResponse.kt index f128142..66569f1 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/dto/ChatRoomResponse.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/dto/ChatRoomResponse.kt @@ -7,16 +7,21 @@ import java.time.ZonedDateTime data class ChatRoomResponse( val id: Long?, val title: String, + val displayTitle: String, val guideId: Long, val userId: Long, val updatedAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")), val lastMessageId: Long?, ) { companion object { - fun from(room: ChatRoom): ChatRoomResponse { + fun from( + room: ChatRoom, + displayTitle: String? = null, + ): ChatRoomResponse { return ChatRoomResponse( id = room.id, title = room.title, + displayTitle = displayTitle ?: room.title, guideId = room.guideId, userId = room.userId, updatedAt = room.updatedAt, diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt index 778045a..5e36457 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt @@ -1,5 +1,7 @@ package com.back.koreaTravelGuide.domain.userChat.chatroom.service +import com.back.koreaTravelGuide.domain.user.entity.User +import com.back.koreaTravelGuide.domain.user.repository.UserRepository import com.back.koreaTravelGuide.domain.userChat.chatmessage.repository.ChatMessageRepository import com.back.koreaTravelGuide.domain.userChat.chatroom.dto.ChatRoomListResponse import com.back.koreaTravelGuide.domain.userChat.chatroom.dto.ChatRoomResponse @@ -17,6 +19,7 @@ import java.util.NoSuchElementException class ChatRoomService( private val roomRepository: ChatRoomRepository, private val messageRepository: ChatMessageRepository, + private val userRepository: UserRepository, ) { @Transactional fun createOneToOneRoom( @@ -74,7 +77,11 @@ class ChatRoomService( val (cursorUpdatedAt, cursorRoomId) = parseCursor(cursor) val pageable = PageRequest.of(0, limit) val rooms = roomRepository.findPagedByMember(requesterId, cursorUpdatedAt, cursorRoomId, pageable) - val roomResponses = rooms.map(ChatRoomResponse::from) + val usersById = loadUsersFor(rooms) + val roomResponses = + rooms.map { room -> + toResponse(room, requesterId, usersById) + } val nextCursor = if (roomResponses.size < limit) { null @@ -90,6 +97,52 @@ class ChatRoomService( ) } + @Transactional(readOnly = true) + fun getResponse( + roomId: Long, + requesterId: Long, + ): ChatRoomResponse { + val room = get(roomId, requesterId) + val usersById = loadUsersFor(listOf(room)) + return toResponse(room, requesterId, usersById) + } + + fun toResponse( + room: ChatRoom, + viewerId: Long, + ): ChatRoomResponse { + val usersById = loadUsersFor(listOf(room)) + return toResponse(room, viewerId, usersById) + } + + private fun toResponse( + room: ChatRoom, + viewerId: Long, + cachedUsers: Map, + ): ChatRoomResponse { + val displayTitle = buildDisplayTitle(room, viewerId, cachedUsers) + return ChatRoomResponse.from(room, displayTitle) + } + + private fun loadUsersFor(rooms: Collection): Map { + val ids = rooms.flatMap { listOf(it.guideId, it.userId) }.toSet() + if (ids.isEmpty()) { + return emptyMap() + } + return userRepository.findAllById(ids).associateBy { it.id!! } + } + + private fun buildDisplayTitle( + room: ChatRoom, + viewerId: Long, + cachedUsers: Map, + ): String { + val guideNickname = cachedUsers[room.guideId]?.nickname ?: "Guide-${room.guideId}" + val userNickname = cachedUsers[room.userId]?.nickname ?: "User-${room.userId}" + val counterpartName = if (viewerId == room.guideId) userNickname else guideNickname + return "${counterpartName}님과의 채팅" + } + private fun parseCursor(cursor: String?): Pair { if (cursor.isNullOrBlank()) { return null to null diff --git a/src/test/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomControllerTest.kt b/src/test/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomControllerTest.kt index 104d45d..21b1bd3 100644 --- a/src/test/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomControllerTest.kt +++ b/src/test/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomControllerTest.kt @@ -125,6 +125,7 @@ class ChatRoomControllerTest { .andExpect(status().isOk) .andExpect(jsonPath("$.data.rooms.length()").value(3)) .andExpect(jsonPath("$.data.rooms[0].id").value(existingRoom.id!!.toInt())) + .andExpect(jsonPath("$.data.rooms[0].displayTitle").value("${guide.nickname}님과의 채팅")) .andReturn() val firstCursorNode = objectMapper.readTree(firstPage.response.contentAsString)["data"]["nextCursor"] @@ -139,7 +140,8 @@ class ChatRoomControllerTest { ) .andExpect(status().isOk) .andExpect(jsonPath("$.data.rooms.length()").value(3)) - .andExpect(jsonPath("$.data.rooms[0].id").exists()) // 도커 환경에서 정렬 순서가 다를 수 있어 ID 존재만 확인 + .andExpect(jsonPath("$.data.rooms[0].id").value(extraRooms[2].id!!.toInt())) + .andExpect(jsonPath("$.data.rooms[0].displayTitle").value("guide3님과의 채팅")) } @Test @@ -156,6 +158,7 @@ class ChatRoomControllerTest { .andExpect(jsonPath("$.data.id").value(existingRoom.id!!.toInt())) .andExpect(jsonPath("$.data.guideId").value(guide.id!!.toInt())) .andExpect(jsonPath("$.data.userId").value(guest.id!!.toInt())) + .andExpect(jsonPath("$.data.displayTitle").value("${guide.nickname}님과의 채팅")) } @Test @@ -168,6 +171,7 @@ class ChatRoomControllerTest { .andExpect(status().isOk) .andExpect(jsonPath("$.data.id").value(existingRoom.id!!.toInt())) .andExpect(jsonPath("$.data.title").value(existingRoom.title)) + .andExpect(jsonPath("$.data.displayTitle").value("${guest.nickname}님과의 채팅")) } @Test