From 5a58fe90777d25861bf0c17134aa6a348c99da9a Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 25 Aug 2025 14:28:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20?= =?UTF-8?q?CloudFront=20=EC=84=9C=EB=AA=85=20URL=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20DTO=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 조회 가능한(보유/무료/결제완료) 이미지 메시지의 이미지 URL을 ImageContentCloudFront.generateSignedURL(만료 5분)로 생성 - 접근 불가(미보유, 유료 미구매) 이미지 메시지는 기존 공개 호스트 URL(블러/스냅샷 경로) 유지 - ChatRoomService에 ImageContentCloudFront를 주입하고, toChatMessageItemDto에서 이미지 URL/hasAccess 결정 로직 단일화 - enterChatRoom, getChatMessages, sendMessage 경로의 중복된 DTO 매핑 로직 제거 - purchaseMessage 결제 완료 시 forceHasAccess=true로 접근 가능 DTO 반환 --- .../chat/room/service/ChatRoomService.kt | 104 ++++-------------- 1 file changed, 22 insertions(+), 82 deletions(-) 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 55d58bb..35b2ed8 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 @@ -48,6 +48,7 @@ class ChatRoomService( private val characterService: ChatCharacterService, private val characterImageService: CharacterImageService, private val canPaymentService: kr.co.vividnext.sodalive.can.payment.CanPaymentService, + private val imageCloudFront: kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront, @Value("\${weraser.api-key}") private val apiKey: String, @@ -332,40 +333,7 @@ class ChatRoomService( } val messagesAsc = fetched.sortedBy { it.createdAt } - val items = messagesAsc.map { msg -> - val sender = msg.participant - val profilePath = when (sender.participantType) { - ParticipantType.USER -> sender.member?.profileImage - ParticipantType.CHARACTER -> sender.character?.imagePath - } - val senderImageUrl = "$imageHost/${profilePath ?: "profile/default-profile.png"}" - val createdAtMillis = msg.createdAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli() - ?: 0L - ChatMessageItemDto( - messageId = msg.id!!, - message = msg.message, - profileImageUrl = senderImageUrl, - mine = sender.member?.id == member.id, - createdAt = createdAtMillis, - messageType = msg.messageType.name, - imageUrl = msg.imagePath?.let { "$imageHost/$it" }, - price = msg.price, - hasAccess = if (msg.messageType == ChatMessageType.IMAGE) { - if (msg.price == null) { - true - } else { - msg.characterImage?.id?.let { - characterImageService.isOwnedImageByMember( - it, - member.id!! - ) - } ?: true - } - } else { - true - } - ) - } + val items = messagesAsc.map { toChatMessageItemDto(it, member) } return ChatRoomEnterResponse( roomId = room.id!!, @@ -512,40 +480,7 @@ class ChatRoomService( // createdAt 오름차순으로 정렬하여 반환 val messagesAsc = fetched.sortedBy { it.createdAt } - val items = messagesAsc.map { msg -> - val sender = msg.participant - val profilePath = when (sender.participantType) { - ParticipantType.USER -> sender.member?.profileImage - ParticipantType.CHARACTER -> sender.character?.imagePath - } - val senderImageUrl = "$imageHost/${profilePath ?: "profile/default-profile.png"}" - val createdAtMillis = msg.createdAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli() - ?: 0L - ChatMessageItemDto( - messageId = msg.id!!, - message = msg.message, - profileImageUrl = senderImageUrl, - mine = sender.member?.id == member.id, - createdAt = createdAtMillis, - messageType = msg.messageType.name, - imageUrl = msg.imagePath?.let { "$imageHost/$it" }, - price = msg.price, - hasAccess = if (msg.messageType == ChatMessageType.IMAGE) { - if (msg.price == null) { - true - } else { - msg.characterImage?.id?.let { - characterImageService.isOwnedImageByMember( - it, - member.id!! - ) - } ?: true - } - } else { - true - } - ) - } + val items = messagesAsc.map { toChatMessageItemDto(it, member) } return ChatMessagesPageResponse( messages = items, @@ -643,19 +578,7 @@ class ChatRoomService( ) ) - val imageDto = ChatMessageItemDto( - messageId = imageMsg.id!!, - message = imageMsg.message, - profileImageUrl = senderImageUrl, - mine = false, - createdAt = imageMsg.createdAt?.atZone(ZoneId.systemDefault())?.toInstant() - ?.toEpochMilli() - ?: 0L, - messageType = ChatMessageType.IMAGE.name, - imageUrl = imageMsg.imagePath?.let { "$imageHost/$it" }, - price = imageMsg.price, - hasAccess = owned || imageMsg.price == null - ) + val imageDto = toChatMessageItemDto(imageMsg, member) return listOf(textDto, imageDto) } @@ -687,6 +610,23 @@ class ChatRoomService( } else { true } + val expirationMs = 5L * 60L * 1000L + val resolvedImageUrl: String? = if (msg.messageType == ChatMessageType.IMAGE) { + val path = if (hasAccess) { + msg.characterImage?.imagePath ?: msg.imagePath + } else { + msg.imagePath + } + path?.let { p -> + if (hasAccess) { + imageCloudFront.generateSignedURL(p, expirationMs) + } else { + "$imageHost/$p" + } + } + } else { + null + } return ChatMessageItemDto( messageId = msg.id!!, message = msg.message, @@ -694,7 +634,7 @@ class ChatRoomService( mine = sender.member?.id == member.id, createdAt = createdAtMillis, messageType = msg.messageType.name, - imageUrl = msg.imagePath?.let { "$imageHost/$it" }, + imageUrl = resolvedImageUrl, price = msg.price, hasAccess = hasAccess )