feat(chat): 채팅방 메시지 전송 API 구현
This commit is contained in:
parent
002f2c2834
commit
4b3463e97c
|
@ -1,6 +1,7 @@
|
|||
package kr.co.vividnext.sodalive.chat.room.controller
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.room.dto.CreateChatRoomRequest
|
||||
import kr.co.vividnext.sodalive.chat.room.dto.SendChatMessageRequest
|
||||
import kr.co.vividnext.sodalive.chat.room.service.ChatRoomService
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
|
@ -116,4 +117,22 @@ class ChatRoomController(
|
|||
chatRoomService.leaveChatRoom(member, chatRoomId)
|
||||
ApiResponse.ok(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 채팅방 메시지 전송 API
|
||||
* - 참여 여부 검증(미참여시 "잘못된 접근입니다")
|
||||
* - 외부 API 호출 (/api/chat, POST) 재시도 최대 3회
|
||||
* - 성공 시 내 메시지/캐릭터 메시지 저장 후 캐릭터 메시지 리스트 반환
|
||||
*/
|
||||
@PostMapping("/{chatRoomId}/send")
|
||||
fun sendMessage(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
@PathVariable chatRoomId: Long,
|
||||
@RequestBody request: SendChatMessageRequest
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||
|
||||
ApiResponse.ok(chatRoomService.sendMessage(member, chatRoomId, request.message))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,3 +93,44 @@ data class ExternalCharacterData(
|
|||
val age: String,
|
||||
val gender: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 채팅 메시지 전송 요청 DTO
|
||||
*/
|
||||
data class SendChatMessageRequest(
|
||||
val message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 채팅 메시지 전송 응답 DTO (캐릭터 메시지 리스트)
|
||||
*/
|
||||
data class SendChatMessageResponse(
|
||||
val characterMessages: List<ChatMessageItemDto>
|
||||
)
|
||||
|
||||
/**
|
||||
* 외부 API 채팅 전송 응답 DTO
|
||||
*/
|
||||
data class ExternalChatSendResponse(
|
||||
val success: Boolean,
|
||||
val message: String?,
|
||||
val data: ExternalChatSendData?
|
||||
)
|
||||
|
||||
/**
|
||||
* 외부 API 채팅 전송 데이터 DTO
|
||||
*/
|
||||
data class ExternalChatSendData(
|
||||
val sessionId: String,
|
||||
val characterResponse: ExternalCharacterMessage
|
||||
)
|
||||
|
||||
/**
|
||||
* 외부 API 캐릭터 메시지 DTO
|
||||
*/
|
||||
data class ExternalCharacterMessage(
|
||||
val id: String,
|
||||
val content: String,
|
||||
val timestamp: String,
|
||||
val messageType: String
|
||||
)
|
||||
|
|
|
@ -18,6 +18,14 @@ interface CharacterChatParticipantRepository : JpaRepository<CharacterChatPartic
|
|||
member: Member
|
||||
): CharacterChatParticipant?
|
||||
|
||||
/**
|
||||
* 특정 채팅방에 특정 타입(CHARACTER/USER)으로 활성 상태인 참여자 찾기
|
||||
*/
|
||||
fun findByChatRoomAndParticipantTypeAndIsActiveTrue(
|
||||
chatRoom: CharacterChatRoom,
|
||||
participantType: ParticipantType
|
||||
): CharacterChatParticipant?
|
||||
|
||||
/**
|
||||
* 특정 채팅방의 활성 USER 참여자 수
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.chat.room.service
|
|||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||
import kr.co.vividnext.sodalive.chat.room.CharacterChatMessage
|
||||
import kr.co.vividnext.sodalive.chat.room.CharacterChatParticipant
|
||||
import kr.co.vividnext.sodalive.chat.room.CharacterChatRoom
|
||||
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
||||
|
@ -9,8 +10,10 @@ import kr.co.vividnext.sodalive.chat.room.dto.ChatMessageItemDto
|
|||
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.ExternalChatSendResponse
|
||||
import kr.co.vividnext.sodalive.chat.room.dto.ExternalChatSessionCreateResponse
|
||||
import kr.co.vividnext.sodalive.chat.room.dto.ExternalChatSessionGetResponse
|
||||
import kr.co.vividnext.sodalive.chat.room.dto.SendChatMessageResponse
|
||||
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
|
||||
|
@ -357,4 +360,131 @@ class ChatRoomService(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun sendMessage(member: Member, chatRoomId: Long, message: String): SendChatMessageResponse {
|
||||
// 1) 방 존재 확인
|
||||
val room = chatRoomRepository.findById(chatRoomId).orElseThrow {
|
||||
SodaException("채팅방을 찾을 수 없습니다.")
|
||||
}
|
||||
// 2) 참여 여부 확인 (USER)
|
||||
val myParticipant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
|
||||
?: throw SodaException("잘못된 접근입니다")
|
||||
// 3) 캐릭터 참여자 조회
|
||||
val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue(
|
||||
room,
|
||||
ParticipantType.CHARACTER
|
||||
) ?: throw SodaException("잘못된 접근입니다")
|
||||
|
||||
val character = characterParticipant.character
|
||||
?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
|
||||
// 4) 외부 API 호출 준비
|
||||
val userId = generateUserId(member.id!!)
|
||||
val sessionId = room.sessionId
|
||||
val characterUUID = character.characterUUID
|
||||
|
||||
// 5) 외부 API 호출 (최대 3회 재시도)
|
||||
val characterReply = callExternalApiForChatSendWithRetry(userId, characterUUID, message, sessionId)
|
||||
|
||||
// 6) 내 메시지 저장
|
||||
val myMsgEntity = CharacterChatMessage(
|
||||
message = message,
|
||||
chatRoom = room,
|
||||
participant = myParticipant,
|
||||
isActive = true
|
||||
)
|
||||
messageRepository.save(myMsgEntity)
|
||||
|
||||
// 7) 캐릭터 메시지 저장
|
||||
val characterMsgEntity = CharacterChatMessage(
|
||||
message = characterReply,
|
||||
chatRoom = room,
|
||||
participant = characterParticipant,
|
||||
isActive = true
|
||||
)
|
||||
val savedCharacterMsg = messageRepository.save(characterMsgEntity)
|
||||
|
||||
// 8) 응답 DTO 구성 (캐릭터 메시지 리스트 반환 - 단일 요소)
|
||||
val profilePath = characterParticipant.character?.imagePath
|
||||
val defaultPath = profilePath ?: "profile/default-profile.png"
|
||||
val imageUrl = "$imageHost/$defaultPath"
|
||||
val dto = ChatMessageItemDto(
|
||||
messageId = savedCharacterMsg.id!!,
|
||||
message = savedCharacterMsg.message,
|
||||
profileImageUrl = imageUrl,
|
||||
mine = false
|
||||
)
|
||||
|
||||
return SendChatMessageResponse(characterMessages = listOf(dto))
|
||||
}
|
||||
|
||||
private fun callExternalApiForChatSendWithRetry(
|
||||
userId: String,
|
||||
characterUUID: String,
|
||||
message: String,
|
||||
sessionId: String
|
||||
): String {
|
||||
val maxAttempts = 3
|
||||
var attempt = 0
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++
|
||||
try {
|
||||
return callExternalApiForChatSend(userId, characterUUID, message, sessionId)
|
||||
} catch (e: Exception) {
|
||||
log.warn("[chat] 외부 채팅 전송 실패 attempt={}, error={}", attempt, e.message)
|
||||
}
|
||||
}
|
||||
log.error("[chat] 외부 채팅 전송 최종 실패 attempts={}", maxAttempts)
|
||||
throw SodaException("메시지 전송을 실패했습니다.")
|
||||
}
|
||||
|
||||
private fun callExternalApiForChatSend(
|
||||
userId: String,
|
||||
characterUUID: String,
|
||||
message: String,
|
||||
sessionId: String
|
||||
): String {
|
||||
val factory = SimpleClientHttpRequestFactory()
|
||||
factory.setConnectTimeout(20000)
|
||||
factory.setReadTimeout(20000)
|
||||
|
||||
val restTemplate = RestTemplate(factory)
|
||||
|
||||
val headers = HttpHeaders()
|
||||
headers.set("x-api-key", apiKey)
|
||||
headers.contentType = MediaType.APPLICATION_JSON
|
||||
|
||||
val requestBody = mapOf(
|
||||
"userId" to userId,
|
||||
"characterId" to characterUUID,
|
||||
"message" to message,
|
||||
"sessionId" to sessionId
|
||||
)
|
||||
|
||||
val httpEntity = HttpEntity(requestBody, headers)
|
||||
|
||||
val response = restTemplate.exchange(
|
||||
"$apiUrl/api/chat",
|
||||
HttpMethod.POST,
|
||||
httpEntity,
|
||||
String::class.java
|
||||
)
|
||||
|
||||
val objectMapper = ObjectMapper()
|
||||
val apiResponse = objectMapper.readValue(
|
||||
response.body,
|
||||
ExternalChatSendResponse::class.java
|
||||
)
|
||||
|
||||
if (!apiResponse.success) {
|
||||
throw SodaException(apiResponse.message ?: "메시지 전송을 실패했습니다.")
|
||||
}
|
||||
val data = apiResponse.data ?: throw SodaException("메시지 전송을 실패했습니다.")
|
||||
val characterContent = data.characterResponse.content
|
||||
if (characterContent.isBlank()) {
|
||||
throw SodaException("메시지 전송을 실패했습니다.")
|
||||
}
|
||||
return characterContent
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue