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 aeb96ec05..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 @@ -1,11 +1,13 @@ 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 @@ -16,14 +18,13 @@ 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") @@ -34,4 +35,18 @@ class AiController( val prompt = aiFacade.processAiMessage(userId, request.toChatRoomId(), request.toMessage()) return ResponseHelper.success(AiChatMessageResponse.of(prompt)) } + + @PostMapping("/ai/chat/clone") + fun cloneDirectChatRoom( + @CurrentUser userId: UserId, + @RequestBody request: ClonePromptRequest, + ): SuccessResponseEntity { + val (userMessage, aiMessage) = aiFacade.cloneChatAsUserFromChatRoom( + requestingUserId = userId, + sourceChatRoomId = ChatRoomId.of(request.sourceChatRoomId), + targetAiChatRoomId = ChatRoomId.of(request.aiChatRoomId), + userPrompt = request.prompt, + ) + 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 new file mode 100644 index 000000000..0a106234b --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/dto/request/chat/ClonePromptRequest.kt @@ -0,0 +1,7 @@ +package org.chewing.v1.dto.request.chat + +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..b5c7418ac --- /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..60f91c767 --- /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/build.gradle.kts b/build.gradle.kts index d46f1a561..7ed900c98 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,6 +65,7 @@ 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..4223b13e3 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,34 @@ class AiFacade( ): ChatRoomId { return aiChatRoomService.createAiChatRoom(userId) } + + fun cloneChatAsUserFromChatRoom( + requestingUserId: UserId, + sourceChatRoomId: ChatRoomId, + targetAiChatRoomId: ChatRoomId, + userPrompt: String, + ): Pair { + // 1. 현재 채팅방에서 상대방 ID 추출 + val directChatRoom = directChatRoomService.getDirectChatRoom(requestingUserId, sourceChatRoomId) + val friendUserId = directChatRoom.roomInfo.friendId + + // 2. 해당 채팅방 로그 중, 상대방이 작성한 메시지만 추출 + val friendMessages = chatLogService.getChatLogsBySender(sourceChatRoomId, friendUserId) + + // 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) + + // 4. 클론용 프롬프트 생성 + val aiGeneratedPrompt = aiPromptService.promptClone(friendMessages, userPrompt) + + // 5. AI 응답을 실제 채팅방에 저장 + val aiResponseSeq = aiChatRoomService.increaseDirectChatRoomSequence(targetAiChatRoomId) + val aiUserId = aiUserGenerator.getAiUserId() + + val aiMessage = chatLogService.aiMessage(sourceChatRoomId, aiUserId, aiResponseSeq, aiGeneratedPrompt, ChatRoomType.AI, SenderType.AI) + + 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 34432baa9..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 @@ -7,6 +7,8 @@ import org.chewing.v1.model.ai.PromptRole 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 +31,31 @@ 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..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 @@ -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..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,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() } + } }