diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt index 4a6ffad..a235ae0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt @@ -298,17 +298,49 @@ class ChatRoomService( @Transactional fun enterChatRoom(member: Member, chatRoomId: Long, characterImageId: Long? = null): ChatRoomEnterResponse { - val room = chatRoomRepository.findByIdAndIsActiveTrue(chatRoomId) - ?: throw SodaException("채팅방을 찾을 수 없습니다.") - // 참여 여부 검증 - participantRepository.findByChatRoomAndMemberAndIsActiveTrue(room, member) - ?: throw SodaException("잘못된 접근입니다") + // 1) 활성 여부 무관하게 방 조회 + val baseRoom = chatRoomRepository.findById(chatRoomId).orElseThrow { + SodaException("채팅방을 찾을 수 없습니다.") + } - // 캐릭터 참여자 조회 - val characterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue( - room, + // 2) 기본 방 기준 참여/활성 여부 확인 + val isActiveRoom = baseRoom.isActive + val isMyActiveParticipation = + participantRepository.findByChatRoomAndMemberAndIsActiveTrue(baseRoom, member) != null + + // 3) 기본 방의 캐릭터 식별 (활성 우선, 없으면 컬렉션에서 검색) + val baseCharacterParticipant = participantRepository.findByChatRoomAndParticipantTypeAndIsActiveTrue( + baseRoom, 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 ?: throw SodaException("오류가 발생했습니다. 다시 시도해 주세요.") @@ -321,13 +353,13 @@ class ChatRoomService( characterType = character.characterType.name ) - // 메시지 최신 20개 조회 후 createdAt 오름차순으로 반환 + // 메시지 최신 20개 조회 후 createdAt 오름차순으로 반환 (effectiveRoom 기준) 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 hasMore: Boolean = if (nextCursor != null) { - messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, nextCursor) + messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(effectiveRoom, nextCursor) } else { false } @@ -339,38 +371,47 @@ class ChatRoomService( val quotaStatus = chatQuotaService.applyRefillOnEnterAndGetStatus(member.id!!) // 선택적 캐릭터 이미지 서명 URL 생성 처리 - val signedUrl: String? = try { - if (characterImageId != null) { - val img = characterImageService.getById(characterImageId) - // 동일 캐릭터 소속 및 활성 검증 - if (img.chatCharacter.id == character.id && img.isActive) { - val owned = - (img.imagePriceCan == 0L) || characterImageService.isOwnedImageByMember(img.id!!, member.id!!) - if (owned) { - val expiration = 5L * 60L * 1000L // 5분 - imageCloudFront.generateSignedURL(img.imagePath, expiration) + // 요구사항: baseRoom이 조건 불만족으로 동일 캐릭터의 내 활성 방으로 라우팅된 경우(bg 이미지 요청 무시)에는 null로 처리 + val signedUrl: String? = + if (effectiveRoom.id != baseRoom.id) { + null + } else { + try { + if (characterImageId != null) { + val img = characterImageService.getById(characterImageId) + // 동일 캐릭터 소속 및 활성 검증 + if (img.chatCharacter.id == character.id && img.isActive) { + val owned = + (img.imagePriceCan == 0L) || characterImageService.isOwnedImageByMember( + img.id!!, + member.id!! + ) + if (owned) { + val expiration = 5L * 60L * 1000L // 5분 + imageCloudFront.generateSignedURL(img.imagePath, expiration) + } else { + null + } + } else { + null + } } else { null } - } else { + } catch (e: Exception) { + // 문제가 있어도 입장 자체는 가능해야 하므로 로그만 남기고 null 반환 + log.warn( + "[chat] enter: signed url generation failed. roomId={}, imageId={}, reason={}", + effectiveRoom.id, + characterImageId, + e.message + ) null } - } else { - null } - } catch (e: Exception) { - // 문제가 있어도 입장 자체는 가능해야 하므로 로그만 남기고 null 반환 - log.warn( - "[chat] enter: signed url generation failed. roomId={}, imageId={}, reason={}", - room.id, - characterImageId, - e.message - ) - null - } return ChatRoomEnterResponse( - roomId = room.id!!, + roomId = effectiveRoom.id!!, character = characterDto, messages = items, hasMoreMessages = hasMore,