feat(chat): 채팅방 나가기 API 구현
This commit is contained in:
parent
830e41dfa3
commit
1509ee0729
|
@ -29,7 +29,7 @@ class CharacterChatParticipant(
|
||||||
@JoinColumn(name = "character_id")
|
@JoinColumn(name = "character_id")
|
||||||
val character: ChatCharacter? = null,
|
val character: ChatCharacter? = null,
|
||||||
|
|
||||||
val isActive: Boolean = true
|
var isActive: Boolean = true
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
@OneToMany(mappedBy = "participant", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "participant", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
|
||||||
val messages: MutableList<CharacterChatMessage> = mutableListOf()
|
val messages: MutableList<CharacterChatMessage> = mutableListOf()
|
||||||
|
|
|
@ -78,4 +78,23 @@ class ChatRoomController(
|
||||||
val isActive = chatRoomService.isMyRoomSessionActive(member, chatRoomId)
|
val isActive = chatRoomService.isMyRoomSessionActive(member, chatRoomId)
|
||||||
ApiResponse.ok(isActive)
|
ApiResponse.ok(isActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 채팅방 나가기 API
|
||||||
|
* - URL에 chatRoomId 포함
|
||||||
|
* - 내가 참여 중인지 확인 (아니면 "잘못된 접근입니다")
|
||||||
|
* - 내 참여자 isActive=false 처리
|
||||||
|
* - 내가 마지막 USER였다면 외부 API로 세션 종료 호출
|
||||||
|
*/
|
||||||
|
@PostMapping("/{chatRoomId}/leave")
|
||||||
|
fun leaveChatRoom(
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||||
|
@PathVariable chatRoomId: Long
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
|
|
||||||
|
chatRoomService.leaveChatRoom(member, chatRoomId)
|
||||||
|
ApiResponse.ok(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package kr.co.vividnext.sodalive.chat.room.repository
|
package kr.co.vividnext.sodalive.chat.room.repository
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
|
||||||
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.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.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
@ -12,10 +12,6 @@ interface CharacterChatParticipantRepository : JpaRepository<CharacterChatPartic
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 채팅방에 참여 중인 멤버 참여자 찾기
|
* 특정 채팅방에 참여 중인 멤버 참여자 찾기
|
||||||
*
|
|
||||||
* @param chatRoom 채팅방
|
|
||||||
* @param member 멤버
|
|
||||||
* @return 채팅방 참여자 (없으면 null)
|
|
||||||
*/
|
*/
|
||||||
fun findByChatRoomAndMemberAndIsActiveTrue(
|
fun findByChatRoomAndMemberAndIsActiveTrue(
|
||||||
chatRoom: CharacterChatRoom,
|
chatRoom: CharacterChatRoom,
|
||||||
|
@ -23,14 +19,10 @@ interface CharacterChatParticipantRepository : JpaRepository<CharacterChatPartic
|
||||||
): CharacterChatParticipant?
|
): CharacterChatParticipant?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 채팅방에 참여 중인 캐릭터 참여자 찾기
|
* 특정 채팅방의 활성 USER 참여자 수
|
||||||
*
|
|
||||||
* @param chatRoom 채팅방
|
|
||||||
* @param character 캐릭터
|
|
||||||
* @return 채팅방 참여자 (없으면 null)
|
|
||||||
*/
|
*/
|
||||||
fun findByChatRoomAndCharacterAndIsActiveTrue(
|
fun countByChatRoomAndParticipantTypeAndIsActiveTrue(
|
||||||
chatRoom: CharacterChatRoom,
|
chatRoom: CharacterChatRoom,
|
||||||
character: ChatCharacter
|
participantType: ParticipantType
|
||||||
): CharacterChatParticipant?
|
): Long
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import kr.co.vividnext.sodalive.chat.room.repository.CharacterChatParticipantRep
|
||||||
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
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
|
@ -45,6 +46,7 @@ class ChatRoomService(
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
) {
|
) {
|
||||||
|
private val log = LoggerFactory.getLogger(ChatRoomService::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 채팅방 생성 또는 조회
|
* 채팅방 생성 또는 조회
|
||||||
|
@ -250,4 +252,76 @@ class ChatRoomService(
|
||||||
throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.")
|
throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun leaveChatRoom(member: Member, chatRoomId: Long) {
|
||||||
|
val room = chatRoomRepository.findById(chatRoomId).orElseThrow {
|
||||||
|
SodaException("채팅방을 찾을 수 없습니다.")
|
||||||
|
}
|
||||||
|
val participant = participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member)
|
||||||
|
?: throw SodaException("잘못된 접근입니다")
|
||||||
|
|
||||||
|
// 1) 나가기 처리
|
||||||
|
participant.isActive = false
|
||||||
|
participantRepository.save(participant)
|
||||||
|
|
||||||
|
// 2) 남은 USER 참여자 수 확인
|
||||||
|
val userCount = participantRepository.countByChatRoomAndParticipantTypeAndIsActiveTrue(
|
||||||
|
room,
|
||||||
|
ParticipantType.USER
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3) 내가 마지막 USER였다면 외부 세션 종료
|
||||||
|
if (userCount == 0L) {
|
||||||
|
endExternalSession(room.sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun endExternalSession(sessionId: String) {
|
||||||
|
// 사용자 흐름을 방해하지 않기 위해 실패 시 예외를 던지지 않고 내부 재시도 후 로그만 남깁니다.
|
||||||
|
val maxAttempts = 3
|
||||||
|
var attempt = 0
|
||||||
|
while (attempt < maxAttempts) {
|
||||||
|
attempt++
|
||||||
|
try {
|
||||||
|
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 httpEntity = HttpEntity(null, headers)
|
||||||
|
|
||||||
|
val response = restTemplate.exchange(
|
||||||
|
"$apiUrl/api/session/$sessionId/end",
|
||||||
|
HttpMethod.PUT,
|
||||||
|
httpEntity,
|
||||||
|
String::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
val objectMapper = ObjectMapper()
|
||||||
|
val node = objectMapper.readTree(response.body)
|
||||||
|
val success = node.get("success")?.asBoolean(false) ?: false
|
||||||
|
if (success) {
|
||||||
|
log.info("[chat] 외부 세션 종료 성공: sessionId={}, attempt={}", sessionId, attempt)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
"[chat] 외부 세션 종료 응답 실패: sessionId={}, attempt={}, body={}",
|
||||||
|
sessionId,
|
||||||
|
attempt,
|
||||||
|
response.body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("[chat] 외부 세션 종료 중 예외: sessionId={}, attempt={}, message={}", sessionId, attempt, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 최종 실패 로그 (예외 미전파)
|
||||||
|
log.error("[chat] 외부 세션 종료 최종 실패: sessionId={}, attempts={}", sessionId, maxAttempts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue