캐릭터 챗봇 #338

Merged
klaus merged 119 commits from test into main 2025-09-10 06:08:47 +00:00
1 changed files with 77 additions and 36 deletions
Showing only changes of commit a94cf8dad9 - Show all commits

View File

@ -298,17 +298,49 @@ class ChatRoomService(
@Transactional @Transactional
fun enterChatRoom(member: Member, chatRoomId: Long, characterImageId: Long? = null): ChatRoomEnterResponse { fun enterChatRoom(member: Member, chatRoomId: Long, characterImageId: Long? = null): ChatRoomEnterResponse {
val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) // 1) 활성 여부 무관하게 방 조회
?: throw SodaException("채팅방을 찾을 수 없습니다.") val baseRoom = chatRoomRepository.findById(chatRoomId).orElseThrow {
// 참여 여부 검증 SodaException("채팅방을 찾을 수 없습니다.")
participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) }
?: throw SodaException("잘못된 접근입니다")
// 캐릭터 참여자 조회 // 2) 기본 방 기준 참여/활성 여부 확인
val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue( val isActiveRoom = baseRoom.isActive
room, val isMyActiveParticipation =
participantRepository.findByChatRoomAndMemberAndIsActiveTrue(baseRoom, member) != null
// 3) 기본 방의 캐릭터 식별 (활성 우선, 없으면 컬렉션에서 검색)
val baseCharacterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue(
baseRoom,
ParticipantType.CHARACTER ParticipantType.CHARACTER
) ?: throw SodaException("잘못된 접근입니다") ) ?: baseRoom.participants.firstOrNull {
it.participantType == ParticipantType.CHARACTER
} ?: throw SodaException("잘못된 접근입니다")
val baseCharacter = baseCharacterParticipant.character
?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.")
// 4) 유효한 입장 대상 방 결정
val effectiveRoom: ChatRoom = if (isActiveRoom && isMyActiveParticipation) {
baseRoom
} else {
// 동일 캐릭터 + 내가 참여 중인 활성 방을 찾는다
val alt = chatRoomRepository.findActiveChatRoomByMemberAndCharacter(member, baseCharacter)
alt ?: ( // 대체 방이 없으면 기존과 동일하게 예외 처리
if (!isActiveRoom) {
throw SodaException("채팅방을 찾을 수 없습니다.")
} else {
throw SodaException("잘못된 접근입니다")
}
)
}
// 5) 응답 구성 시에는 effectiveRoom의 캐릭터(활성 우선) 사용
val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue(
effectiveRoom,
ParticipantType.CHARACTER
) ?: effectiveRoom.participants.firstOrNull {
it.participantType == ParticipantType.CHARACTER
} ?: throw SodaException("잘못된 접근입니다")
val character = characterParticipant.character val character = characterParticipant.character
?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") ?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.")
@ -321,13 +353,13 @@ class ChatRoomService(
characterType = character.characterType.name characterType = character.characterType.name
) )
// 메시지 최신 20개 조회 후 createdAt 오름차순으로 반환 // 메시지 최신 20개 조회 후 createdAt 오름차순으로 반환 (effectiveRoom 기준)
val pageable = PageRequest.of(0, 20) val pageable = PageRequest.of(0, 20)
val fetched = messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(room, pageable) val fetched = messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(effectiveRoom, pageable)
val nextCursor: Long? = fetched.minByOrNull { it.id ?: Long.MAX_VALUE }?.id val nextCursor: Long? = fetched.minByOrNull { it.id ?: Long.MAX_VALUE }?.id
val hasMore: Boolean = if (nextCursor != null) { val hasMore: Boolean = if (nextCursor != null) {
messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, nextCursor) messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(effectiveRoom, nextCursor)
} else { } else {
false false
} }
@ -339,13 +371,21 @@ class ChatRoomService(
val quotaStatus = chatQuotaService.applyRefillOnEnterAndGetStatus(member.id!!) val quotaStatus = chatQuotaService.applyRefillOnEnterAndGetStatus(member.id!!)
// 선택적 캐릭터 이미지 서명 URL 생성 처리 // 선택적 캐릭터 이미지 서명 URL 생성 처리
val signedUrl: String? = try { // 요구사항: baseRoom이 조건 불만족으로 동일 캐릭터의 내 활성 방으로 라우팅된 경우(bg 이미지 요청 무시)에는 null로 처리
val signedUrl: String? =
if (effectiveRoom.id != baseRoom.id) {
null
} else {
try {
if (characterImageId != null) { if (characterImageId != null) {
val img = characterImageService.getById(characterImageId) val img = characterImageService.getById(characterImageId)
// 동일 캐릭터 소속 및 활성 검증 // 동일 캐릭터 소속 및 활성 검증
if (img.chatCharacter.id == character.id && img.isActive) { if (img.chatCharacter.id == character.id && img.isActive) {
val owned = val owned =
(img.imagePriceCan == 0L) || characterImageService.isOwnedImageByMember(img.id!!, member.id!!) (img.imagePriceCan == 0L) || characterImageService.isOwnedImageByMember(
img.id!!,
member.id!!
)
if (owned) { if (owned) {
val expiration = 5L * 60L * 1000L // 5분 val expiration = 5L * 60L * 1000L // 5분
imageCloudFront.generateSignedURL(img.imagePath, expiration) imageCloudFront.generateSignedURL(img.imagePath, expiration)
@ -362,15 +402,16 @@ class ChatRoomService(
// 문제가 있어도 입장 자체는 가능해야 하므로 로그만 남기고 null 반환 // 문제가 있어도 입장 자체는 가능해야 하므로 로그만 남기고 null 반환
log.warn( log.warn(
"[chat] enter: signed url generation failed. roomId={}, imageId={}, reason={}", "[chat] enter: signed url generation failed. roomId={}, imageId={}, reason={}",
room.id, effectiveRoom.id,
characterImageId, characterImageId,
e.message e.message
) )
null null
} }
}
return ChatRoomEnterResponse( return ChatRoomEnterResponse(
roomId = room.id!!, roomId = effectiveRoom.id!!,
character = characterDto, character = characterDto,
messages = items, messages = items,
hasMoreMessages = hasMore, hasMoreMessages = hasMore,