From 0825ad3f2ee1425e44b4d10acd0748043554d22c Mon Sep 17 00:00:00 2001 From: youngho kim Date: Wed, 28 May 2025 15:56:12 +0900 Subject: [PATCH 1/3] insert AI clone-chatting main to dev --- .../chewing/v1/controller/ai/AiController.kt | 18 +++++++++++ .../v1/dto/request/chat/ClonePromptRequest.kt | 3 ++ build.gradle.kts | 2 ++ .../kotlin/org/chewing/v1/facade/AiFacade.kt | 26 ++++++++++++++++ .../v1/implementation/ai/AiPromptGenerator.kt | 31 +++++++++++++++++++ .../v1/repository/chat/ChatLogRepository.kt | 2 ++ .../chewing/v1/service/ai/AiPromptService.kt | 5 +++ .../chewing/v1/service/chat/ChatLogService.kt | 5 +++ .../chewing/v1/service/ChatLogServiceTest.kt | 1 + .../mongo/chat/log/ChatLogRepositoryImpl.kt | 16 ++++++++++ 10 files changed, 109 insertions(+) create mode 100644 api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt diff --git a/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt b/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt index aeb96ec05..02b81e7c2 100644 --- a/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt +++ b/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt @@ -1,6 +1,7 @@ package org.chewing.v1.controller.ai import org.chewing.v1.dto.request.chat.ChatRequest +import org.chewing.v1.dto.request.chat.ClonePromptRequest import org.chewing.v1.dto.response.chat.AiChatMessageResponse import org.chewing.v1.facade.AiFacade import org.chewing.v1.model.chat.room.ChatRoomId @@ -10,6 +11,7 @@ import org.chewing.v1.util.aliases.SuccessResponseEntity import org.chewing.v1.util.helper.ResponseHelper import org.chewing.v1.util.security.CurrentUser import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -34,4 +36,20 @@ class AiController( val prompt = aiFacade.processAiMessage(userId, request.toChatRoomId(), request.toMessage()) return ResponseHelper.success(AiChatMessageResponse.of(prompt)) } + + @PostMapping("/ai/chat/clone/from-room/{chatRoomId}") + fun cloneFromDirectChatRoom( + @CurrentUser userId: UserId, + @PathVariable chatRoomId: String, + @RequestBody request: ClonePromptRequest, + ): SuccessResponseEntity { + val result = aiFacade.cloneChatAsUserFromChatRoom( + requester = userId, + chatRoomId = ChatRoomId.of(chatRoomId), + prompt = request.prompt + ) + return ResponseHelper.success(AiChatMessageResponse.of(result)) + } + + } diff --git a/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt b/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt new file mode 100644 index 000000000..888adf1f8 --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt @@ -0,0 +1,3 @@ +package org.chewing.v1.dto.request.chat + +data class ClonePromptRequest(val prompt: String) diff --git a/build.gradle.kts b/build.gradle.kts index d46f1a561..c03f6bb36 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,6 +65,8 @@ subprojects { //env implementation("me.paulschwarz:spring-dotenv:4.0.0") + implementation("org.springframework.boot:spring-boot-starter-web") + } tasks { 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 bfc3af18e..4bfc01e83 100644 --- a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -9,6 +9,7 @@ import org.chewing.v1.model.user.UserId import org.chewing.v1.service.ai.AiPromptService import org.chewing.v1.service.chat.AiChatRoomService import org.chewing.v1.service.chat.ChatLogService +import org.chewing.v1.service.chat.DirectChatRoomService import org.springframework.stereotype.Service @Service @@ -17,6 +18,7 @@ class AiFacade( private val aiPromptService: AiPromptService, private val chatLogService: ChatLogService, private val aiUserGenerator: AiUserGenerator, + private val directChatRoomService: DirectChatRoomService, ) { fun processAiMessage( userId: UserId, @@ -38,4 +40,28 @@ class AiFacade( ): ChatRoomId { return aiChatRoomService.createAiChatRoom(userId) } + + fun cloneChatAsUserFromChatRoom( + requester: UserId, + chatRoomId: ChatRoomId, + prompt: String, + ): ChatAiMessage { + // 1. 현재 채팅방에서 상대방 ID 추출 + val directChatRoom = directChatRoomService.getDirectChatRoom(requester, chatRoomId) + val targetUserId = directChatRoom.roomInfo.friendId + + // 2. 해당 채팅방 로그 중, targetUserId가 작성한 메시지만 추출 + val targetLogs = chatLogService.getChatLogsBySender(chatRoomId, targetUserId) + + // 3. 프롬프트 생성 + val clonePrompt = aiPromptService.promptClone(targetLogs, prompt) + + // 4. AI 응답을 현재 채팅방에 저장 + val sequence = aiChatRoomService.increaseDirectChatRoomSequence(chatRoomId) + val aiUserId = aiUserGenerator.getAiUserId() + + return chatLogService.aiMessage(chatRoomId, aiUserId, sequence, clonePrompt, ChatRoomType.DIRECT, SenderType.AI) + } + + } 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 34432baa9..3b50fe468 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 @@ -4,9 +4,12 @@ import org.chewing.v1.error.ConflictException import org.chewing.v1.error.ErrorCode import org.chewing.v1.model.ai.Prompt import org.chewing.v1.model.ai.PromptRole +import org.chewing.v1.model.ai.PromptType import org.chewing.v1.model.ai.TextPrompt import org.chewing.v1.model.chat.log.ChatAiLog import org.chewing.v1.model.chat.log.ChatLog +import org.chewing.v1.model.chat.log.ChatNormalLog +import org.chewing.v1.model.chat.log.ChatReplyLog import org.chewing.v1.model.chat.member.SenderType import org.springframework.stereotype.Component @@ -29,4 +32,32 @@ 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 + else -> null + } + text?.let { + TextPrompt.of( + role = PromptRole.USER, + text = it + ) + } + } + + val finalPrompt = TextPrompt.of( + role = PromptRole.USER, + text = "채팅 로그를 분석해서 대화 문맥에 따라 다음 대화에 채팅로그의 사용자가 너라고 생각하고 사용자의 말투와 똑같이 답해줘, 예를 들어 사용자가 반말중이면 반말하고," + + "공룡이 주제인거 같으면 공룡에 대해서 답변하면 된다. :\n\n$prompt" + ) + + return messagePrompts + finalPrompt + } + } diff --git a/domain/src/main/kotlin/org/chewing/v1/repository/chat/ChatLogRepository.kt b/domain/src/main/kotlin/org/chewing/v1/repository/chat/ChatLogRepository.kt index 6d8f51eb3..996b5d541 100755 --- a/domain/src/main/kotlin/org/chewing/v1/repository/chat/ChatLogRepository.kt +++ b/domain/src/main/kotlin/org/chewing/v1/repository/chat/ChatLogRepository.kt @@ -4,6 +4,7 @@ import org.chewing.v1.model.chat.log.ChatLog import org.chewing.v1.model.chat.log.UnReadTarget import org.chewing.v1.model.chat.message.ChatMessage import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.user.UserId interface ChatLogRepository { fun readChatMessages(chatRoomId: ChatRoomId, sequence: Int, joinSequence: Int): List @@ -15,4 +16,5 @@ interface ChatLogRepository { fun readChatKeyWordMessages(chatRoomId: ChatRoomId, keyword: String): List fun readUnreadChatLogs(targets: List): List fun readLatestChatMessage(chatRoomId: ChatRoomId): ChatLog? + fun readChatLogsBySender(chatRoomId: ChatRoomId, senderId: UserId): List } diff --git a/domain/src/main/kotlin/org/chewing/v1/service/ai/AiPromptService.kt b/domain/src/main/kotlin/org/chewing/v1/service/ai/AiPromptService.kt index 6afb8b6ec..06d7c0bb3 100644 --- a/domain/src/main/kotlin/org/chewing/v1/service/ai/AiPromptService.kt +++ b/domain/src/main/kotlin/org/chewing/v1/service/ai/AiPromptService.kt @@ -14,4 +14,9 @@ class AiPromptService( val prompts = aiPromptGenerator.generateChatLogPrompts(chatlogs) return aiSender.sendPrompt(prompts) } + + fun promptClone(chatlogs: List, prompt: String): String { + val chatStylePrompt = aiPromptGenerator.generateClonePrompt(chatlogs, prompt) + return aiSender.sendPrompt(chatStylePrompt) + } } diff --git a/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt b/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt index edf3d9fd2..728363eeb 100755 --- a/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt +++ b/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt @@ -19,6 +19,7 @@ import org.chewing.v1.model.media.FileCategory import org.chewing.v1.model.media.FileData import org.chewing.v1.model.media.Media import org.chewing.v1.model.user.UserId +import org.chewing.v1.repository.chat.ChatLogRepository import org.springframework.stereotype.Service @Service @@ -29,6 +30,7 @@ class ChatLogService( private val chatGenerator: ChatGenerator, private val chatRemover: ChatRemover, private val chatValidator: ChatValidator, + private val chatLogRepository: ChatLogRepository ) { fun uploadFiles(fileDataList: List, userId: UserId): List { return fileHandler.handleNewFiles(userId, fileDataList, FileCategory.CHAT) @@ -194,4 +196,7 @@ class ChatLogService( fun getChatLog(messageId: String): ChatLog { return chatReader.readChatMessage(messageId) } + fun getChatLogsBySender(chatRoomId: ChatRoomId, senderId: UserId): List { + return chatLogRepository.readChatLogsBySender(chatRoomId, senderId) + } } diff --git a/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt b/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt index 18ed126a4..8ffc4041d 100755 --- a/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt +++ b/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt @@ -36,6 +36,7 @@ class ChatLogServiceTest { chatGenerator, chatRemover, chatValidator, + chatLogRepository ) @Test diff --git a/storage/src/main/kotlin/org/chewing/v1/repository/mongo/chat/log/ChatLogRepositoryImpl.kt b/storage/src/main/kotlin/org/chewing/v1/repository/mongo/chat/log/ChatLogRepositoryImpl.kt index 1252ca743..917ae94c9 100644 --- a/storage/src/main/kotlin/org/chewing/v1/repository/mongo/chat/log/ChatLogRepositoryImpl.kt +++ b/storage/src/main/kotlin/org/chewing/v1/repository/mongo/chat/log/ChatLogRepositoryImpl.kt @@ -4,11 +4,13 @@ import org.chewing.v1.model.chat.log.ChatLog import org.chewing.v1.model.chat.log.UnReadTarget import org.chewing.v1.model.chat.message.ChatMessage import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.user.UserId import org.chewing.v1.mongoentity.ChatMessageMongoEntity import org.chewing.v1.mongoentity.LatestChatMessageWrapper import org.chewing.v1.mongorepository.ChatLogMongoRepository import org.chewing.v1.repository.chat.ChatLogRepository import org.chewing.v1.util.SortType +import org.springframework.data.domain.Sort import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.aggregation.Aggregation import org.springframework.data.mongodb.core.query.Criteria @@ -116,4 +118,18 @@ internal class ChatLogRepositoryImpl( .limit(1) return mongoTemplate.findOne(query, ChatMessageMongoEntity::class.java)?.toChatLog() } + + override fun readChatLogsBySender(chatRoomId: ChatRoomId, senderId: UserId): List { + val criteria = Criteria + .where("chatRoomId").`is`(chatRoomId.id) + .and("senderId").`is`(senderId.id) + .and("type").`in`("NORMAL", "REPLY", "AI") + + val query = Query(criteria) + .with(Sort.by(Sort.Direction.ASC, "sequence")) + .limit(100) // 적절한 범위로 조정 + + return mongoTemplate.find(query, ChatMessageMongoEntity::class.java) + .map { it.toChatLog() } + } } From d39ee219ea556f30ba6d5d7a0947975daa1ba08e Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Thu, 29 May 2025 17:06:39 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20AI=20ChatRoom=20adoc=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/docs/asciidoc/Ai-API.adoc | 28 +++ api/src/docs/asciidoc/index.adoc | 2 + .../chewing/v1/controller/ai/AiController.kt | 29 ++- .../v1/dto/request/chat/ClonePromptRequest.kt | 8 +- .../dto/response/chat/ChatRoomIdResponse.kt | 6 +- .../chat/PairAiChatMessageResponse.kt | 24 +++ .../chewing/v1/controller/AiControllerTest.kt | 171 ++++++++++++++++++ .../kotlin/org/chewing/v1/facade/AiFacade.kt | 35 ++-- 8 files changed, 270 insertions(+), 33 deletions(-) create mode 100644 api/src/docs/asciidoc/Ai-API.adoc create mode 100644 api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt create mode 100644 api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt diff --git a/api/src/docs/asciidoc/Ai-API.adoc b/api/src/docs/asciidoc/Ai-API.adoc new file mode 100644 index 000000000..0618b1e25 --- /dev/null +++ b/api/src/docs/asciidoc/Ai-API.adoc @@ -0,0 +1,28 @@ +[[Ai-API]] += Ai API + +== 1. Ai 채팅방 생성 + +=== 요청(Request) +include::{snippets}/ai-controller-test/create-ai-chat-room/http-request.adoc[] + +=== 응답(Response) +include::{snippets}/ai-controller-test/create-ai-chat-room/http-response.adoc[] + +=== 응답 필드 설명(Response Fields) +include::{snippets}/ai-controller-test/create-ai-chat-room/response-fields.adoc[] + +== 2. AI 클론 채팅 + +=== 요청(Request) +include::{snippets}/ai-controller-test/clone-ai-chat-room/http-request.adoc[] + +=== 요청 필드 설명(Request Fields) +include::{snippets}/ai-controller-test/clone-ai-chat-room/request-fields.adoc[] + +=== 응답(Response) +include::{snippets}/ai-controller-test/clone-ai-chat-room/http-response.adoc[] + +=== 응답 필드 설명(Response Fields) +include::{snippets}/ai-controller-test/clone-ai-chat-room/response-fields.adoc[] + diff --git a/api/src/docs/asciidoc/index.adoc b/api/src/docs/asciidoc/index.adoc index 81a6ab62b..478b298ad 100755 --- a/api/src/docs/asciidoc/index.adoc +++ b/api/src/docs/asciidoc/index.adoc @@ -85,3 +85,5 @@ include::Chat-Log-API.adoc[] include::Notification-API.adoc[] include::Report-API.adoc[] + +include::Ai-API.adoc[] diff --git a/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt b/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt index 02b81e7c2..310599aa1 100644 --- a/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt +++ b/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt @@ -3,29 +3,28 @@ package org.chewing.v1.controller.ai import org.chewing.v1.dto.request.chat.ChatRequest import org.chewing.v1.dto.request.chat.ClonePromptRequest import org.chewing.v1.dto.response.chat.AiChatMessageResponse +import org.chewing.v1.dto.response.chat.ChatRoomIdResponse +import org.chewing.v1.dto.response.chat.PairAiChatMessageResponse import org.chewing.v1.facade.AiFacade import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.user.UserId -import org.chewing.v1.service.chat.AiChatRoomService import org.chewing.v1.util.aliases.SuccessResponseEntity import org.chewing.v1.util.helper.ResponseHelper import org.chewing.v1.util.security.CurrentUser import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @Controller class AiController( private val aiFacade: AiFacade, - private val aiChatRoomService: AiChatRoomService, ) { @PostMapping("/ai/chat/room") fun createAiChatRoom( @CurrentUser userId: UserId, - ): SuccessResponseEntity { + ): SuccessResponseEntity { val chatRoomId = aiFacade.produceAiChatRoom(userId) - return ResponseHelper.success(chatRoomId) + return ResponseHelper.successCreate(ChatRoomIdResponse.of(chatRoomId)) } @PostMapping("/ai/chat/room/prompt") @@ -37,19 +36,17 @@ class AiController( return ResponseHelper.success(AiChatMessageResponse.of(prompt)) } - @PostMapping("/ai/chat/clone/from-room/{chatRoomId}") - fun cloneFromDirectChatRoom( + @PostMapping("/ai/chat/clone") + fun cloneDirectChatRoom( @CurrentUser userId: UserId, - @PathVariable chatRoomId: String, @RequestBody request: ClonePromptRequest, - ): SuccessResponseEntity { - val result = aiFacade.cloneChatAsUserFromChatRoom( - requester = userId, - chatRoomId = ChatRoomId.of(chatRoomId), - prompt = request.prompt + ): SuccessResponseEntity { + val (userMessage, aiMessage) = aiFacade.cloneChatAsUserFromChatRoom( + requestingUserId = userId, + sourceChatRoomId = ChatRoomId.of(request.sourceChatRoomId), + targetAiChatRoomId = ChatRoomId.of(request.aiChatRoomId), + userPrompt = request.prompt, ) - return ResponseHelper.success(AiChatMessageResponse.of(result)) + return ResponseHelper.success(PairAiChatMessageResponse.of(userMessage, aiMessage)) } - - } diff --git a/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt b/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt index 888adf1f8..d6fa9268b 100644 --- a/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt +++ b/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt @@ -1,3 +1,9 @@ package org.chewing.v1.dto.request.chat -data class ClonePromptRequest(val prompt: String) +import org.chewing.v1.model.chat.room.ChatRoomId + +data class ClonePromptRequest( + val prompt: String, + val sourceChatRoomId: String, + val aiChatRoomId: String, +) diff --git a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatRoomIdResponse.kt b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatRoomIdResponse.kt index f648d30d2..182553520 100755 --- a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatRoomIdResponse.kt +++ b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatRoomIdResponse.kt @@ -1,11 +1,13 @@ package org.chewing.v1.dto.response.chat +import org.chewing.v1.model.chat.room.ChatRoomId + class ChatRoomIdResponse( val chatRoomId: String, ) { companion object { - fun from(chatRoomId: String): ChatRoomIdResponse { - return ChatRoomIdResponse(chatRoomId) + fun of(chatRoomId: ChatRoomId): ChatRoomIdResponse { + return ChatRoomIdResponse(chatRoomId.id) } } } diff --git a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt new file mode 100644 index 000000000..5ec670415 --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt @@ -0,0 +1,24 @@ +package org.chewing.v1.dto.response.chat + +import org.chewing.v1.model.chat.message.ChatAiMessage + +data class PairAiChatMessageResponse( + val userMessage: AiChatMessageResponse, + val aiMessage: AiChatMessageResponse, +) { + companion object { + fun of( + userMessage: ChatAiMessage, + aiMessage: ChatAiMessage, + ): PairAiChatMessageResponse { + return PairAiChatMessageResponse( + userMessage = AiChatMessageResponse.of( + userMessage + ), + aiMessage = AiChatMessageResponse.of( + aiMessage + ), + ) + } + } +} diff --git a/api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt b/api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt new file mode 100644 index 000000000..5e04d3daf --- /dev/null +++ b/api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt @@ -0,0 +1,171 @@ + +package org.chewing.v1.controller + +import io.mockk.every +import io.mockk.mockk +import org.chewing.v1.RestDocsTest +import org.chewing.v1.RestDocsUtils.requestAccessTokenFields +import org.chewing.v1.RestDocsUtils.requestPreprocessor +import org.chewing.v1.RestDocsUtils.responsePreprocessor +import org.chewing.v1.controller.ai.AiController +import org.chewing.v1.dto.request.chat.ClonePromptRequest +import org.chewing.v1.facade.AiFacade +import org.chewing.v1.model.chat.member.SenderType +import org.chewing.v1.model.chat.message.ChatAiMessage +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.chat.room.ChatRoomSequence +import org.chewing.v1.model.chat.room.ChatRoomType +import org.chewing.v1.model.user.UserId +import org.chewing.v1.util.handler.GlobalExceptionHandler +import org.chewing.v1.util.security.UserArgumentResolver +import org.hamcrest.CoreMatchers.equalTo +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.http.HttpStatus +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.test.context.ActiveProfiles +import java.time.LocalDateTime + +@ActiveProfiles("test") +class AiControllerTest : RestDocsTest() { + + private lateinit var aiFacade: AiFacade + private lateinit var aiController: AiController + private lateinit var exceptionHandler: GlobalExceptionHandler + private lateinit var userArgumentResolver: UserArgumentResolver + + @BeforeEach + fun setUp() { + aiFacade = mockk() + exceptionHandler = GlobalExceptionHandler() + userArgumentResolver = UserArgumentResolver() + aiController = AiController(aiFacade) + mockMvc = mockController(aiController, exceptionHandler, userArgumentResolver) + + val userId = UserId.of("testUserId") + val authentication = UsernamePasswordAuthenticationToken(userId, null) + SecurityContextHolder.getContext().authentication = authentication + } + + @Test + @DisplayName("AI 채팅방 생성") + fun createAiChatRoom() { + val chatRoomId = ChatRoomId.of("ai-room-001") + every { aiFacade.produceAiChatRoom(any()) } returns chatRoomId + + given() + .setupAuthenticatedJsonRequest() + .post("/ai/chat/room") + .then() + .statusCode(HttpStatus.CREATED.value()) + .apply { + body("status", equalTo(201)) + body("data.chatRoomId", equalTo(chatRoomId.id)) + } + .apply( + document( + "{class-name}/{method-name}", + requestPreprocessor(), + responsePreprocessor(), + responseFields( + fieldWithPath("status").description("응답 상태"), + fieldWithPath("data.chatRoomId").description("AI 채팅방 ID") + ), + requestAccessTokenFields(), + ) + ) + } + + @Test + @DisplayName("일반 채팅방 복제 프롬프트 요청") + fun cloneAiChatRoom() { + val chatRoomId = ChatRoomId.of("ai-room-001") + val request = ClonePromptRequest( + prompt = "안녕", + sourceChatRoomId = "chatroom-001", + aiChatRoomId = "ai-room-001" + ) + val aiMessage = ChatAiMessage.of( + messageId = "msg-002", + chatRoomId = chatRoomId, + chatRoomType = ChatRoomType.AI, + senderId = UserId.of("ai-user"), + timestamp = LocalDateTime.now(), + roomSequence = ChatRoomSequence.of(chatRoomId,2), + text = "안녕하세요!", + senderType = SenderType.AI + ) + val userMessage = ChatAiMessage.of( + messageId = "msg-001", + chatRoomId = chatRoomId, + chatRoomType = ChatRoomType.AI, + senderId = UserId.of("testUserId"), + timestamp = LocalDateTime.now(), + roomSequence = ChatRoomSequence.of(chatRoomId, 1), + text = request.prompt, + senderType = SenderType.USER + ) + every { + aiFacade.cloneChatAsUserFromChatRoom(any(), any(), any(), any()) + } returns Pair(userMessage, aiMessage) + + given() + .setupAuthenticatedJsonRequest() + .body(request) + .post("/ai/chat/clone") + .then() + .statusCode(HttpStatus.OK.value()) + .body("status", equalTo(200)) + .apply { + body("data.userMessage.messageId", equalTo(userMessage.messageId)) + body("data.userMessage.type", equalTo(userMessage.type.name.lowercase())) + body("data.userMessage.senderId", equalTo(userMessage.senderId.id)) + body("data.userMessage.timestamp", equalTo(userMessage.timestamp.toString())) + body("data.userMessage.seqNumber", equalTo(userMessage.roomSequence.sequence)) + body("data.userMessage.text", equalTo(userMessage.text)) + body("data.userMessage.senderType", equalTo(userMessage.senderType.name.lowercase())) + + body("data.aiMessage.messageId", equalTo(aiMessage.messageId)) + body("data.aiMessage.type", equalTo(aiMessage.type.name.lowercase())) + body("data.aiMessage.senderId", equalTo(aiMessage.senderId.id)) + body("data.aiMessage.timestamp", equalTo(aiMessage.timestamp.toString())) + body("data.aiMessage.seqNumber", equalTo(aiMessage.roomSequence.sequence)) + body("data.aiMessage.text", equalTo(aiMessage.text)) + body("data.aiMessage.senderType", equalTo(aiMessage.senderType.name.lowercase())) + } + .apply( + document( + "{class-name}/{method-name}", + requestPreprocessor(), + responsePreprocessor(), + requestFields( + fieldWithPath("prompt").description("사용자 입력 프롬프트"), + fieldWithPath("sourceChatRoomId").description("복제할 원본 채팅방 ID"), + fieldWithPath("aiChatRoomId").description("AI 채팅방 ID") + ), + requestAccessTokenFields(), + responseFields( + fieldWithPath("status").description("응답 상태"), + fieldWithPath("data.userMessage.messageId").description("AI 메시지 ID"), + fieldWithPath("data.userMessage.type").description("메시지 타입"), + fieldWithPath("data.userMessage.senderId").description("메시지 발신자 ID"), + fieldWithPath("data.userMessage.timestamp").description("메시지 타임스탬프"), + fieldWithPath("data.userMessage.seqNumber").description("채팅방 내 메시지 순서"), + fieldWithPath("data.userMessage.text").description("메시지 내용"), + fieldWithPath("data.userMessage.senderType").description("메시지 발신자 타입"), + fieldWithPath("data.aiMessage.messageId").description("AI 메시지 ID"), + fieldWithPath("data.aiMessage.type").description("메시지 타입"), + fieldWithPath("data.aiMessage.senderId").description("메시지 발신자 ID"), + fieldWithPath("data.aiMessage.timestamp").description("메시지 타임스탬프"), + fieldWithPath("data.aiMessage.seqNumber").description("채팅방 내 메시지 순서"), + fieldWithPath("data.aiMessage.text").description("메시지 내용"), + fieldWithPath("data.aiMessage.senderType").description("메시지 발신자 타입") + ) + ) + ) + } +} 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 4bfc01e83..ebb5806c9 100644 --- a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -42,26 +42,33 @@ class AiFacade( } fun cloneChatAsUserFromChatRoom( - requester: UserId, - chatRoomId: ChatRoomId, - prompt: String, - ): ChatAiMessage { + requestingUserId: UserId, + sourceChatRoomId: ChatRoomId, + targetAiChatRoomId: ChatRoomId, + userPrompt: String, + ): Pair { // 1. 현재 채팅방에서 상대방 ID 추출 - val directChatRoom = directChatRoomService.getDirectChatRoom(requester, chatRoomId) - val targetUserId = directChatRoom.roomInfo.friendId + val directChatRoom = directChatRoomService.getDirectChatRoom(requestingUserId, sourceChatRoomId) + val friendUserId = directChatRoom.roomInfo.friendId + + // 2. 해당 채팅방 로그 중, 상대방이 작성한 메시지만 추출 + val friendMessages = chatLogService.getChatLogsBySender(sourceChatRoomId, friendUserId) - // 2. 해당 채팅방 로그 중, targetUserId가 작성한 메시지만 추출 - val targetLogs = chatLogService.getChatLogsBySender(chatRoomId, targetUserId) + // 3. 사용자 입력 메시지를 AI 채팅방에 저장 + val aiChatRoom = aiChatRoomService.getAiChatRoom(sourceChatRoomId, requestingUserId) + val userMessageSeq = aiChatRoomService.increaseDirectChatRoomSequence(aiChatRoom.chatRoomId) + val userMessage = chatLogService.aiMessage(aiChatRoom.chatRoomId, requestingUserId, userMessageSeq, userPrompt, ChatRoomType.AI, SenderType.USER) - // 3. 프롬프트 생성 - val clonePrompt = aiPromptService.promptClone(targetLogs, prompt) + // 4. 클론용 프롬프트 생성 + val aiGeneratedPrompt = aiPromptService.promptClone(friendMessages, userPrompt) - // 4. AI 응답을 현재 채팅방에 저장 - val sequence = aiChatRoomService.increaseDirectChatRoomSequence(chatRoomId) + // 5. AI 응답을 실제 채팅방에 저장 + val aiResponseSeq = aiChatRoomService.increaseDirectChatRoomSequence(targetAiChatRoomId) val aiUserId = aiUserGenerator.getAiUserId() - return chatLogService.aiMessage(chatRoomId, aiUserId, sequence, clonePrompt, ChatRoomType.DIRECT, SenderType.AI) - } + val aiMessage = chatLogService.aiMessage(sourceChatRoomId, aiUserId, aiResponseSeq, aiGeneratedPrompt, ChatRoomType.AI, SenderType.AI) + return Pair(userMessage,aiMessage) + } } From d01331f0255baca021766a60423fb5d16162db02 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Thu, 29 May 2025 17:08:39 +0900 Subject: [PATCH 3/3] ktlint: format --- .../v1/dto/request/chat/ClonePromptRequest.kt | 2 -- .../chat/PairAiChatMessageResponse.kt | 4 ++-- .../chewing/v1/controller/AiControllerTest.kt | 20 +++++++++---------- build.gradle.kts | 1 - .../kotlin/org/chewing/v1/facade/AiFacade.kt | 5 ++--- .../v1/implementation/ai/AiPromptGenerator.kt | 6 ++---- .../chewing/v1/service/chat/ChatLogService.kt | 2 +- .../chewing/v1/service/ChatLogServiceTest.kt | 2 +- 8 files changed, 18 insertions(+), 24 deletions(-) diff --git a/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt b/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt index d6fa9268b..0a106234b 100644 --- a/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt +++ b/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt @@ -1,7 +1,5 @@ package org.chewing.v1.dto.request.chat -import org.chewing.v1.model.chat.room.ChatRoomId - data class ClonePromptRequest( val prompt: String, val sourceChatRoomId: String, diff --git a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt index 5ec670415..b5c7418ac 100644 --- a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt +++ b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/PairAiChatMessageResponse.kt @@ -13,10 +13,10 @@ data class PairAiChatMessageResponse( ): PairAiChatMessageResponse { return PairAiChatMessageResponse( userMessage = AiChatMessageResponse.of( - userMessage + userMessage, ), aiMessage = AiChatMessageResponse.of( - aiMessage + aiMessage, ), ) } diff --git a/api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt b/api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt index 5e04d3daf..60f91c767 100644 --- a/api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt +++ b/api/src/test/kotlin/org/chewing/v1/controller/AiControllerTest.kt @@ -73,10 +73,10 @@ class AiControllerTest : RestDocsTest() { responsePreprocessor(), responseFields( fieldWithPath("status").description("응답 상태"), - fieldWithPath("data.chatRoomId").description("AI 채팅방 ID") + fieldWithPath("data.chatRoomId").description("AI 채팅방 ID"), ), requestAccessTokenFields(), - ) + ), ) } @@ -87,7 +87,7 @@ class AiControllerTest : RestDocsTest() { val request = ClonePromptRequest( prompt = "안녕", sourceChatRoomId = "chatroom-001", - aiChatRoomId = "ai-room-001" + aiChatRoomId = "ai-room-001", ) val aiMessage = ChatAiMessage.of( messageId = "msg-002", @@ -95,9 +95,9 @@ class AiControllerTest : RestDocsTest() { chatRoomType = ChatRoomType.AI, senderId = UserId.of("ai-user"), timestamp = LocalDateTime.now(), - roomSequence = ChatRoomSequence.of(chatRoomId,2), + roomSequence = ChatRoomSequence.of(chatRoomId, 2), text = "안녕하세요!", - senderType = SenderType.AI + senderType = SenderType.AI, ) val userMessage = ChatAiMessage.of( messageId = "msg-001", @@ -107,7 +107,7 @@ class AiControllerTest : RestDocsTest() { timestamp = LocalDateTime.now(), roomSequence = ChatRoomSequence.of(chatRoomId, 1), text = request.prompt, - senderType = SenderType.USER + senderType = SenderType.USER, ) every { aiFacade.cloneChatAsUserFromChatRoom(any(), any(), any(), any()) @@ -145,7 +145,7 @@ class AiControllerTest : RestDocsTest() { requestFields( fieldWithPath("prompt").description("사용자 입력 프롬프트"), fieldWithPath("sourceChatRoomId").description("복제할 원본 채팅방 ID"), - fieldWithPath("aiChatRoomId").description("AI 채팅방 ID") + fieldWithPath("aiChatRoomId").description("AI 채팅방 ID"), ), requestAccessTokenFields(), responseFields( @@ -163,9 +163,9 @@ class AiControllerTest : RestDocsTest() { fieldWithPath("data.aiMessage.timestamp").description("메시지 타임스탬프"), fieldWithPath("data.aiMessage.seqNumber").description("채팅방 내 메시지 순서"), fieldWithPath("data.aiMessage.text").description("메시지 내용"), - fieldWithPath("data.aiMessage.senderType").description("메시지 발신자 타입") - ) - ) + fieldWithPath("data.aiMessage.senderType").description("메시지 발신자 타입"), + ), + ), ) } } diff --git a/build.gradle.kts b/build.gradle.kts index c03f6bb36..7ed900c98 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -66,7 +66,6 @@ subprojects { //env implementation("me.paulschwarz:spring-dotenv:4.0.0") implementation("org.springframework.boot:spring-boot-starter-web") - } tasks { 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 ebb5806c9..4223b13e3 100644 --- a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -66,9 +66,8 @@ class AiFacade( val aiResponseSeq = aiChatRoomService.increaseDirectChatRoomSequence(targetAiChatRoomId) val aiUserId = aiUserGenerator.getAiUserId() - val aiMessage = chatLogService.aiMessage(sourceChatRoomId, aiUserId, aiResponseSeq, aiGeneratedPrompt, ChatRoomType.AI, SenderType.AI) + val aiMessage = chatLogService.aiMessage(sourceChatRoomId, aiUserId, aiResponseSeq, aiGeneratedPrompt, ChatRoomType.AI, SenderType.AI) - return Pair(userMessage,aiMessage) + return Pair(userMessage, aiMessage) } - } 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 3b50fe468..e4ede2b06 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 @@ -4,7 +4,6 @@ import org.chewing.v1.error.ConflictException import org.chewing.v1.error.ErrorCode import org.chewing.v1.model.ai.Prompt import org.chewing.v1.model.ai.PromptRole -import org.chewing.v1.model.ai.PromptType import org.chewing.v1.model.ai.TextPrompt import org.chewing.v1.model.chat.log.ChatAiLog import org.chewing.v1.model.chat.log.ChatLog @@ -46,7 +45,7 @@ class AiPromptGenerator { text?.let { TextPrompt.of( role = PromptRole.USER, - text = it + text = it, ) } } @@ -54,10 +53,9 @@ class AiPromptGenerator { val finalPrompt = TextPrompt.of( role = PromptRole.USER, text = "채팅 로그를 분석해서 대화 문맥에 따라 다음 대화에 채팅로그의 사용자가 너라고 생각하고 사용자의 말투와 똑같이 답해줘, 예를 들어 사용자가 반말중이면 반말하고," + - "공룡이 주제인거 같으면 공룡에 대해서 답변하면 된다. :\n\n$prompt" + "공룡이 주제인거 같으면 공룡에 대해서 답변하면 된다. :\n\n$prompt", ) return messagePrompts + finalPrompt } - } diff --git a/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt b/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt index 728363eeb..7c4fee7d7 100755 --- a/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt +++ b/domain/src/main/kotlin/org/chewing/v1/service/chat/ChatLogService.kt @@ -30,7 +30,7 @@ class ChatLogService( private val chatGenerator: ChatGenerator, private val chatRemover: ChatRemover, private val chatValidator: ChatValidator, - private val chatLogRepository: ChatLogRepository + private val chatLogRepository: ChatLogRepository, ) { fun uploadFiles(fileDataList: List, userId: UserId): List { return fileHandler.handleNewFiles(userId, fileDataList, FileCategory.CHAT) diff --git a/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt b/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt index 8ffc4041d..a24a18955 100755 --- a/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt +++ b/domain/src/test/kotlin/org/chewing/v1/service/ChatLogServiceTest.kt @@ -36,7 +36,7 @@ class ChatLogServiceTest { chatGenerator, chatRemover, chatValidator, - chatLogRepository + chatLogRepository, ) @Test