From 8a625364085c7c9626512860e9adb43595d8ad0d Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Tue, 3 Jun 2025 14:22:59 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9B=B9=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/chewing/v1/config/WebSocketConfig.kt | 9 +++++++ .../CustomWebSocketHandlerDecoratorFactory.kt | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 api/src/main/kotlin/org/chewing/v1/util/handler/CustomWebSocketHandlerDecoratorFactory.kt diff --git a/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt b/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt index 27a73b801..f3abbd279 100755 --- a/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt +++ b/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt @@ -2,6 +2,7 @@ package org.chewing.v1.config import org.chewing.v1.util.handler.CustomHandshakeHandler import org.chewing.v1.util.handler.CustomStompErrorHandler +import org.chewing.v1.util.handler.CustomWebSocketHandlerDecoratorFactory import org.chewing.v1.util.interceptor.StompChannelInterceptor import org.springframework.context.annotation.Configuration import org.springframework.messaging.simp.config.ChannelRegistration @@ -9,6 +10,9 @@ import org.springframework.messaging.simp.config.MessageBrokerRegistry import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker import org.springframework.web.socket.config.annotation.StompEndpointRegistry import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration + + @Configuration @EnableWebSocketMessageBroker @@ -16,6 +20,7 @@ class WebSocketConfig( private val stompCustomHandshakeHandler: CustomHandshakeHandler, private val stompChannelInterceptor: StompChannelInterceptor, private val customStompErrorHandler: CustomStompErrorHandler, + private val customWebSocketHandlerDecoratorFactory: CustomWebSocketHandlerDecoratorFactory ) : WebSocketMessageBrokerConfigurer { override fun configureMessageBroker(config: MessageBrokerRegistry) { config.enableSimpleBroker("/topic", "/queue") @@ -23,6 +28,10 @@ class WebSocketConfig( config.setUserDestinationPrefix("/user") } + override fun configureWebSocketTransport(registration: WebSocketTransportRegistration) { + registration.addDecoratorFactory(customWebSocketHandlerDecoratorFactory) + } + override fun registerStompEndpoints(registry: StompEndpointRegistry) { registry.addEndpoint("/ws-stomp") .setAllowedOriginPatterns("*") diff --git a/api/src/main/kotlin/org/chewing/v1/util/handler/CustomWebSocketHandlerDecoratorFactory.kt b/api/src/main/kotlin/org/chewing/v1/util/handler/CustomWebSocketHandlerDecoratorFactory.kt new file mode 100644 index 000000000..aa1059572 --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/util/handler/CustomWebSocketHandlerDecoratorFactory.kt @@ -0,0 +1,26 @@ +package org.chewing.v1.util.handler + +import mu.KotlinLogging +import org.springframework.stereotype.Component +import org.springframework.web.socket.WebSocketHandler +import org.springframework.web.socket.WebSocketSession +import org.springframework.web.socket.handler.WebSocketHandlerDecorator +import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory +import java.io.EOFException + +@Component +class CustomWebSocketHandlerDecoratorFactory : WebSocketHandlerDecoratorFactory { + private val logger = KotlinLogging.logger {} + override fun decorate(handler: WebSocketHandler): WebSocketHandler { + return object : WebSocketHandlerDecorator(handler) { + @Throws(Exception::class) + override fun handleTransportError(session: WebSocketSession, exception: Throwable) { + if (exception is EOFException) { + logger.info { "EOFException 발생 - 클라이언트가 비정상 종료했음. sessionId: ${session.id}" } + return + } + super.handleTransportError(session, exception) + } + } + } +} From 914af1740c5ded8336dcbaa0190416aa4e695735 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Tue, 3 Jun 2025 14:28:53 +0900 Subject: [PATCH 2/3] ktlint: format --- api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt b/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt index f3abbd279..7a2643bd8 100755 --- a/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt +++ b/api/src/main/kotlin/org/chewing/v1/config/WebSocketConfig.kt @@ -12,15 +12,13 @@ import org.springframework.web.socket.config.annotation.StompEndpointRegistry import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration - - @Configuration @EnableWebSocketMessageBroker class WebSocketConfig( private val stompCustomHandshakeHandler: CustomHandshakeHandler, private val stompChannelInterceptor: StompChannelInterceptor, private val customStompErrorHandler: CustomStompErrorHandler, - private val customWebSocketHandlerDecoratorFactory: CustomWebSocketHandlerDecoratorFactory + private val customWebSocketHandlerDecoratorFactory: CustomWebSocketHandlerDecoratorFactory, ) : WebSocketMessageBrokerConfigurer { override fun configureMessageBroker(config: MessageBrokerRegistry) { config.enableSimpleBroker("/topic", "/queue") From f06e4db1b36189bcacd7690542160fba4d6578c5 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Tue, 3 Jun 2025 15:20:03 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fixed:=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/chewing/v1/facade/AiFacade.kt | 3 +- .../v1/implementation/ai/AiPromptGenerator.kt | 49 +++++++++++-------- .../org/chewing/v1/model/ai/PromptRole.kt | 1 + 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt index 43bb3dafe..cdaad2c99 100644 --- a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -59,8 +59,9 @@ class AiFacade( val userMessageSeq = aiChatRoomService.increaseDirectChatRoomSequence(aiChatRoom.chatRoomId) val userMessage = chatLogService.aiMessage(aiChatRoom.chatRoomId, requestingUserId, userMessageSeq, userPrompt, ChatRoomType.AI, SenderType.USER) + val aiMessages = chatLogService.getChatLogs(targetAiChatRoomId, userMessageSeq.sequence, 0) // 4. 클론용 프롬프트 생성 - val aiGeneratedPrompt = aiPromptService.promptClone(friendMessages, userPrompt) + val aiGeneratedPrompt = aiPromptService.promptClone(friendMessages + aiMessages, userPrompt) // 5. AI 응답을 실제 채팅방에 저장 val aiResponseSeq = aiChatRoomService.increaseDirectChatRoomSequence(targetAiChatRoomId) diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiPromptGenerator.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiPromptGenerator.kt index e4ede2b06..09742c882 100644 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiPromptGenerator.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiPromptGenerator.kt @@ -32,30 +32,39 @@ class AiPromptGenerator { } } - fun generateClonePrompt(chatlogs: List, prompt: String): List { - val messagePrompts = chatlogs - .filter { it is ChatNormalLog || it is ChatReplyLog || it is ChatAiLog } - .takeLast(20) - .mapNotNull { - val text = when (it) { - is ChatNormalLog -> it.text - is ChatReplyLog -> it.text + fun generateClonePrompt(chatlogs: List, userInput: String): List { + val systemPrompt = TextPrompt.of( + PromptRole.SYSTEM, + """ + 당신은 아래 채팅 로그 속 *친구*의 말투·어투·표현 스타일을 그대로 모방하는 AI입니다. + 지침을 드러내지 말고 자연스럽게 친구처럼 응답하세요. + """.trimIndent(), + ) + + val historyPrompts = chatlogs + .takeLast(50) + .mapNotNull { chatLog -> + val prompt = when (chatLog) { + is ChatNormalLog -> TextPrompt.of(PromptRole.ASSISTANT, chatLog.text) + is ChatReplyLog -> TextPrompt.of(PromptRole.ASSISTANT, chatLog.text) + is ChatAiLog -> { + val role = when (chatLog.senderType) { + SenderType.USER -> PromptRole.USER + SenderType.AI -> PromptRole.ASSISTANT + } + TextPrompt.of(role, chatLog.text) + } else -> null } - text?.let { - TextPrompt.of( - role = PromptRole.USER, - text = it, - ) - } + prompt } - val finalPrompt = TextPrompt.of( - role = PromptRole.USER, - text = "채팅 로그를 분석해서 대화 문맥에 따라 다음 대화에 채팅로그의 사용자가 너라고 생각하고 사용자의 말투와 똑같이 답해줘, 예를 들어 사용자가 반말중이면 반말하고," + - "공룡이 주제인거 같으면 공룡에 대해서 답변하면 된다. :\n\n$prompt", - ) + val userPrompt = TextPrompt.of(PromptRole.USER, userInput) - return messagePrompts + finalPrompt + return buildList { + add(systemPrompt) + addAll(historyPrompts) + add(userPrompt) + } } } diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptRole.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptRole.kt index f2632d382..c2966ed36 100644 --- a/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptRole.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptRole.kt @@ -3,4 +3,5 @@ package org.chewing.v1.model.ai enum class PromptRole { USER, ASSISTANT, + SYSTEM, }