feat(chat): 입장 라우팅 도입 및 라우팅 시 배경 이미지 URL 무시(null)
- baseRoom이 비활성/미참여면 동일 캐릭터의 내 활성 방으로 라우팅해 응답 구성 - 라우팅된 경우 bgImageUrl은 항상 null 처리; 대체 방 없으면 기존 예외 유지
This commit is contained in:
		| @@ -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,38 +371,47 @@ class ChatRoomService( | |||||||
|         val quotaStatus = chatQuotaService.applyRefillOnEnterAndGetStatus(member.id!!) |         val quotaStatus = chatQuotaService.applyRefillOnEnterAndGetStatus(member.id!!) | ||||||
|  |  | ||||||
|         // 선택적 캐릭터 이미지 서명 URL 생성 처리 |         // 선택적 캐릭터 이미지 서명 URL 생성 처리 | ||||||
|         val signedUrl: String? = try { |         // 요구사항: baseRoom이 조건 불만족으로 동일 캐릭터의 내 활성 방으로 라우팅된 경우(bg 이미지 요청 무시)에는 null로 처리 | ||||||
|             if (characterImageId != null) { |         val signedUrl: String? = | ||||||
|                 val img = characterImageService.getById(characterImageId) |             if (effectiveRoom.id != baseRoom.id) { | ||||||
|                 // 동일 캐릭터 소속 및 활성 검증 |                 null | ||||||
|                 if (img.chatCharacter.id == character.id && img.isActive) { |             } else { | ||||||
|                     val owned = |                 try { | ||||||
|                         (img.imagePriceCan == 0L) || characterImageService.isOwnedImageByMember(img.id!!, member.id!!) |                     if (characterImageId != null) { | ||||||
|                     if (owned) { |                         val img = characterImageService.getById(characterImageId) | ||||||
|                         val expiration = 5L * 60L * 1000L // 5분 |                         // 동일 캐릭터 소속 및 활성 검증 | ||||||
|                         imageCloudFront.generateSignedURL(img.imagePath, expiration) |                         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 { |                     } else { | ||||||
|                         null |                         null | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } catch (e: Exception) { | ||||||
|  |                     // 문제가 있어도 입장 자체는 가능해야 하므로 로그만 남기고 null 반환 | ||||||
|  |                     log.warn( | ||||||
|  |                         "[chat] enter: signed url generation failed. roomId={}, imageId={}, reason={}", | ||||||
|  |                         effectiveRoom.id, | ||||||
|  |                         characterImageId, | ||||||
|  |                         e.message | ||||||
|  |                     ) | ||||||
|                     null |                     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( |         return ChatRoomEnterResponse( | ||||||
|             roomId = room.id!!, |             roomId = effectiveRoom.id!!, | ||||||
|             character = characterDto, |             character = characterDto, | ||||||
|             messages = items, |             messages = items, | ||||||
|             hasMoreMessages = hasMore, |             hasMoreMessages = hasMore, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user