From 690fc67b241a23ecbb65ae583963a9c018d1fc98 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Thu, 17 Apr 2025 20:14:27 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20AI=20Client=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chewing/v1/external/ExternalAiClient.kt | 7 +++ .../org/chewing/v1/model/ai/ImagePrompt.kt | 11 ++++ .../kotlin/org/chewing/v1/model/ai/Prompt.kt | 5 ++ .../org/chewing/v1/model/ai/PromptType.kt | 6 ++ .../org/chewing/v1/model/ai/TextPrompt.kt | 10 ++++ .../org/chewing/v1/client/OpenAiClient.kt | 23 ++++++++ .../org/chewing/v1/config/OpenAiConfig.kt | 40 +++++++++++++ .../org/chewing/v1/dto/ChatGPTRequest.kt | 58 +++++++++++++++++++ .../org/chewing/v1/dto/ChatGPTResponse.kt | 15 +++++ .../v1/external/ExternalAiClientImpl.kt | 28 +++++++++ external/src/main/resources/openai.yml | 33 +++++++++++ 11 files changed, 236 insertions(+) create mode 100644 domain/src/main/kotlin/org/chewing/v1/external/ExternalAiClient.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/ai/PromptType.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt create mode 100644 external/src/main/kotlin/org/chewing/v1/client/OpenAiClient.kt create mode 100644 external/src/main/kotlin/org/chewing/v1/config/OpenAiConfig.kt create mode 100644 external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt create mode 100644 external/src/main/kotlin/org/chewing/v1/dto/ChatGPTResponse.kt create mode 100644 external/src/main/kotlin/org/chewing/v1/external/ExternalAiClientImpl.kt create mode 100644 external/src/main/resources/openai.yml diff --git a/domain/src/main/kotlin/org/chewing/v1/external/ExternalAiClient.kt b/domain/src/main/kotlin/org/chewing/v1/external/ExternalAiClient.kt new file mode 100644 index 000000000..a41d78785 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/external/ExternalAiClient.kt @@ -0,0 +1,7 @@ +package org.chewing.v1.external + +import org.chewing.v1.model.ai.Prompt + +interface ExternalAiClient { + suspend fun prompt(prompts: List): String? +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt new file mode 100644 index 000000000..32ce57cef --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt @@ -0,0 +1,11 @@ +package org.chewing.v1.model.ai + +class ImagePrompt private constructor( + val imageUrl: String, +) : Prompt() { + companion object { + fun of(text: String): ImagePrompt = ImagePrompt(text) + } + + override val type = PromptType.IMAGE +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt new file mode 100644 index 000000000..40f0c9303 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt @@ -0,0 +1,5 @@ +package org.chewing.v1.model.ai + +sealed class Prompt { + abstract val type: PromptType +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptType.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptType.kt new file mode 100644 index 000000000..64bc4a321 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptType.kt @@ -0,0 +1,6 @@ +package org.chewing.v1.model.ai + +enum class PromptType { + IMAGE, + TEXT, +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt new file mode 100644 index 000000000..1afa9e0fa --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt @@ -0,0 +1,10 @@ +package org.chewing.v1.model.ai + +class TextPrompt private constructor( + val text: String, +) : Prompt() { + companion object { + fun of(text: String): TextPrompt = TextPrompt(text) + } + override val type = PromptType.TEXT +} diff --git a/external/src/main/kotlin/org/chewing/v1/client/OpenAiClient.kt b/external/src/main/kotlin/org/chewing/v1/client/OpenAiClient.kt new file mode 100644 index 000000000..22d264eaf --- /dev/null +++ b/external/src/main/kotlin/org/chewing/v1/client/OpenAiClient.kt @@ -0,0 +1,23 @@ +package org.chewing.v1.client + +import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.chewing.v1.dto.ChatGPTRequest +import org.chewing.v1.dto.ChatGPTResponse +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient + +@Component +class OpenAiClient( + @Qualifier("openAiWebClient") private val webClient: WebClient, +) { + suspend fun execute(request: ChatGPTRequest): ChatGPTResponse? { + return webClient.post() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(ChatGPTResponse::class.java) + .awaitSingleOrNull() + } +} diff --git a/external/src/main/kotlin/org/chewing/v1/config/OpenAiConfig.kt b/external/src/main/kotlin/org/chewing/v1/config/OpenAiConfig.kt new file mode 100644 index 000000000..30e8455dd --- /dev/null +++ b/external/src/main/kotlin/org/chewing/v1/config/OpenAiConfig.kt @@ -0,0 +1,40 @@ +package org.chewing.v1.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.reactive.function.client.ClientRequest +import org.springframework.web.reactive.function.client.ExchangeFilterFunction +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono + +@Configuration +class OpenAiConfig( + @Value("\${openai.api.key}") + private val openAiKey: String, + @Value("\${openai.api.url}") + private val openAiUrl: String, +) { + + @Bean + fun openAiWebClient(): WebClient = WebClient.builder() + .baseUrl(openAiUrl) + .filter(addAuthorizationHeader()) + .filter(logRequest()) + .build() + + private fun addAuthorizationHeader(): ExchangeFilterFunction = ExchangeFilterFunction.ofRequestProcessor { clientRequest -> + val modifiedRequest = ClientRequest.from(clientRequest) + .header("Authorization", "Bearer $openAiKey") + .build() + Mono.just(modifiedRequest) + } + + private fun logRequest(): ExchangeFilterFunction = ExchangeFilterFunction.ofRequestProcessor { clientRequest -> + Mono.just(clientRequest) + }.andThen( + ExchangeFilterFunction.ofResponseProcessor { clientResponse -> + Mono.just(clientResponse) + }, + ) +} diff --git a/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt b/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt new file mode 100644 index 000000000..083dc21d9 --- /dev/null +++ b/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt @@ -0,0 +1,58 @@ +package org.chewing.v1.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import org.chewing.v1.model.ai.ImagePrompt +import org.chewing.v1.model.ai.Prompt +import org.chewing.v1.model.ai.TextPrompt + +data class ChatGPTRequest( + val model: String, + val messages: List, + @JsonProperty("max_tokens") val maxTokens: Int = 150, +) { + companion object { + fun of( + model: String, + prompts: List, + ): ChatGPTRequest { + return ChatGPTRequest( + model = model, + messages = prompts.map { Message(content = listOf(Message.of(it))) }, + ) + } + } + + data class Message( + val role: String = "user", + val content: List, + ) { + companion object { + fun of(prompt: Prompt): Any = when (prompt) { + is TextPrompt -> ContentText.of(prompt) + is ImagePrompt -> ContentImage.of(prompt) + } + } + + data class ContentText( + val type: String = "text", + val text: String, + ) { + companion object { + fun of(prompt: TextPrompt): ContentText = ContentText(text = prompt.text) + } + } + + data class ContentImage( + val type: String = "image_url", + @JsonProperty("image_url") val imageUrl: Url, + ) { + companion object { + fun of(prompt: ImagePrompt): ContentImage = ContentImage(imageUrl = Url(prompt.imageUrl)) + } + + data class Url( + val url: String, + ) + } + } +} diff --git a/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTResponse.kt b/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTResponse.kt new file mode 100644 index 000000000..22b1ddb38 --- /dev/null +++ b/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTResponse.kt @@ -0,0 +1,15 @@ +package org.chewing.v1.dto + +data class ChatGPTResponse( + var choices: List = listOf(), +) { + data class Choice( + var index: Int = 0, + var message: ChatGptMessage, + ) { + data class ChatGptMessage( + var role: String, + var content: String, + ) + } +} diff --git a/external/src/main/kotlin/org/chewing/v1/external/ExternalAiClientImpl.kt b/external/src/main/kotlin/org/chewing/v1/external/ExternalAiClientImpl.kt new file mode 100644 index 000000000..40d558f8c --- /dev/null +++ b/external/src/main/kotlin/org/chewing/v1/external/ExternalAiClientImpl.kt @@ -0,0 +1,28 @@ +package org.chewing.v1.external + +import mu.KotlinLogging +import org.chewing.v1.client.OpenAiClient +import org.chewing.v1.dto.ChatGPTRequest +import org.chewing.v1.model.ai.Prompt +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component + +@Component +class ExternalAiClientImpl( + @Value("\${openai.model}") private val model: String, + private val openAiClient: OpenAiClient, +) : ExternalAiClient { + + private val logger = KotlinLogging.logger {} + + override suspend fun prompt(prompts: List): String? { + val request = ChatGPTRequest.of(model, prompts) + return try { + val responseBody = openAiClient.execute(request) + responseBody!!.choices[0].message.content.trim() + } catch (e: Exception) { + logger.error("API 호출 실패: ${e.message}", e) + null + } + } +} diff --git a/external/src/main/resources/openai.yml b/external/src/main/resources/openai.yml new file mode 100644 index 000000000..806c8b4bb --- /dev/null +++ b/external/src/main/resources/openai.yml @@ -0,0 +1,33 @@ +spring: + config: + activate: + on-profile: test +openai: + model: test + api: + url: test-url + key: test-key + +--- +spring: + config: + activate: + on-profile: local + +openai: + model: ${OPENAI_API_MODEL} + api: + url: ${OPENAI_API_URL} + key: ${OPENAI_API_KEY} + +--- +spring: + config: + activate: + on-profile: live + +openai: + model: ${OPENAI_API_MODEL} + api: + url: ${OPENAI_API_URL} + key: ${OPENAI_API_KEY} From 62d9fbfd19134dc4e1934c496ae6cd4a20833523 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Sat, 19 Apr 2025 21:17:32 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20AI=20role=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/chewing/v1/error/ErrorCode.kt | 1 + .../chewing/v1/implementation/ai/AiSender.kt | 20 +++++++++++++++++++ .../org/chewing/v1/model/ai/ImagePrompt.kt | 3 ++- .../kotlin/org/chewing/v1/model/ai/Prompt.kt | 1 + .../org/chewing/v1/model/ai/PromptRole.kt | 6 ++++++ .../org/chewing/v1/model/ai/TextPrompt.kt | 3 ++- .../org/chewing/v1/dto/ChatGPTRequest.kt | 9 +++++++-- 7 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiSender.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/ai/PromptRole.kt diff --git a/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt b/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt index 282f64e4d..94a4239dc 100755 --- a/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt +++ b/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt @@ -78,6 +78,7 @@ enum class ErrorCode( INVALID_TYPE("INVALID_1", "잘못된 타입입니다."), + AI_PROMPT_FAILED("AI_1", "AI 프롬프트를 실패하였습니다."), ; companion object { diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiSender.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiSender.kt new file mode 100644 index 000000000..0a1b2acfa --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiSender.kt @@ -0,0 +1,20 @@ +package org.chewing.v1.implementation.ai + +import org.chewing.v1.error.ConflictException +import org.chewing.v1.error.ErrorCode +import org.chewing.v1.external.ExternalAiClient +import org.chewing.v1.model.ai.Prompt +import org.chewing.v1.util.AsyncJobExecutor +import org.springframework.stereotype.Component + +@Component +class AiSender( + private val externalAiClient: ExternalAiClient, + private val asyncJobExecutor: AsyncJobExecutor, +) { + fun sendPrompt(prompts: List): String { + return asyncJobExecutor.executeAsyncReturnJob(prompts) { + externalAiClient.prompt(prompts) ?: throw ConflictException(ErrorCode.AI_PROMPT_FAILED) + } + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt index 32ce57cef..706208e5c 100644 --- a/domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/ImagePrompt.kt @@ -1,10 +1,11 @@ package org.chewing.v1.model.ai class ImagePrompt private constructor( + override val role: PromptRole, val imageUrl: String, ) : Prompt() { companion object { - fun of(text: String): ImagePrompt = ImagePrompt(text) + fun of(role: PromptRole, text: String): ImagePrompt = ImagePrompt(role, text) } override val type = PromptType.IMAGE diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt index 40f0c9303..8ef80f31b 100644 --- a/domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/Prompt.kt @@ -1,5 +1,6 @@ package org.chewing.v1.model.ai sealed class Prompt { + abstract val role: PromptRole abstract val type: PromptType } 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 new file mode 100644 index 000000000..f2632d382 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/PromptRole.kt @@ -0,0 +1,6 @@ +package org.chewing.v1.model.ai + +enum class PromptRole { + USER, + ASSISTANT, +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt b/domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt index 1afa9e0fa..b135db48a 100644 --- a/domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/ai/TextPrompt.kt @@ -1,10 +1,11 @@ package org.chewing.v1.model.ai class TextPrompt private constructor( + override val role: PromptRole, val text: String, ) : Prompt() { companion object { - fun of(text: String): TextPrompt = TextPrompt(text) + fun of(role: PromptRole, text: String): TextPrompt = TextPrompt(role, text) } override val type = PromptType.TEXT } diff --git a/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt b/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt index 083dc21d9..96d00cf40 100644 --- a/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt +++ b/external/src/main/kotlin/org/chewing/v1/dto/ChatGPTRequest.kt @@ -17,13 +17,18 @@ data class ChatGPTRequest( ): ChatGPTRequest { return ChatGPTRequest( model = model, - messages = prompts.map { Message(content = listOf(Message.of(it))) }, + messages = prompts.map { + Message( + role = it.role.name.lowercase(), + content = listOf(Message.of(it)), + ) + }, ) } } data class Message( - val role: String = "user", + val role: String, val content: List, ) { companion object { From dbe7e31450714644e88ca115468bfc4d364feaea Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Tue, 22 Apr 2025 15:45:03 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20AI=20=EC=B1=84=ED=8C=85=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8,=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/dto/response/chat/ChatLogResponse.kt | 20 +++++++ .../kotlin/org/chewing/v1/error/ErrorCode.kt | 1 + .../chat/airoom/AiChatRoomAppender.kt | 15 +++++ .../chat/airoom/AiChatRoomReader.kt | 12 ++++ .../chat/message/ChatGenerator.kt | 21 +++++++ .../notification/NotificationGenerator.kt | 18 ++++-- .../chewing/v1/model/chat/log/ChatAiLog.kt | 43 +++++++++++++ .../v1/model/chat/member/SenderType.kt | 6 ++ .../v1/model/chat/message/ChatAiMessage.kt | 45 ++++++++++++++ .../v1/model/chat/room/ChatRoomType.kt | 1 + .../repository/chat/AiChatRoomRepository.kt | 8 +++ .../org/chewing/v1/service/ai/AiService.kt | 14 +++++ .../v1/service/chat/AiChatRoomService.kt | 18 ++++++ .../chewing/v1/service/chat/ChatLogService.kt | 14 +++++ .../ExternalChatNotificationClientImpl.kt | 7 ++- .../v1/mongoentity/ChatAiMongoEntity.kt | 60 +++++++++++++++++++ .../v1/mongoentity/ChatMessageMongoEntity.kt | 1 + 17 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomAppender.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/chat/member/SenderType.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/chat/message/ChatAiMessage.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/service/ai/AiService.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt create mode 100644 storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatAiMongoEntity.kt diff --git a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatLogResponse.kt b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatLogResponse.kt index 0ecdf1917..3b6155da1 100755 --- a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatLogResponse.kt +++ b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ChatLogResponse.kt @@ -67,6 +67,16 @@ sealed class ChatLogResponse { val comment: String, ) : ChatLogResponse() + data class Ai( + val messageId: String, + val type: String, + val senderId: String, + val timestamp: String, + val seqNumber: Int, + val text: String, + val senderType: String, + ) : ChatLogResponse() + companion object { fun from(chatLog: ChatLog): ChatLogResponse { val formattedTime = chatLog.timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) @@ -133,6 +143,16 @@ sealed class ChatLogResponse { content = chatLog.content, comment = chatLog.comment, ) + + is ChatAiLog -> Ai( + messageId = chatLog.messageId, + type = chatLog.type.name.lowercase(), + senderId = chatLog.senderId.id, + timestamp = formattedTime, + seqNumber = chatLog.roomSequence.sequence, + text = chatLog.text, + senderType = chatLog.senderType.name.lowercase(), + ) } } } diff --git a/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt b/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt index 94a4239dc..789e9f4db 100755 --- a/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt +++ b/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt @@ -79,6 +79,7 @@ enum class ErrorCode( INVALID_TYPE("INVALID_1", "잘못된 타입입니다."), AI_PROMPT_FAILED("AI_1", "AI 프롬프트를 실패하였습니다."), + AI_NOTIFICATION_NOT_SUPPORTED("AI_2", "AI 알림을 지원하지 않습니다."), ; companion object { diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomAppender.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomAppender.kt new file mode 100644 index 000000000..3ffba0b01 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomAppender.kt @@ -0,0 +1,15 @@ +package org.chewing.v1.implementation.chat.airoom + +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.user.UserId +import org.chewing.v1.repository.chat.AiChatRoomRepository +import org.springframework.stereotype.Component + +@Component +class AiChatRoomAppender( + private val aiChatRoomRepository: AiChatRoomRepository, +) { + fun appendChatRoom(userId: UserId): ChatRoomId { + return aiChatRoomRepository.append(userId) + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt new file mode 100644 index 000000000..c746a868e --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt @@ -0,0 +1,12 @@ +package org.chewing.v1.implementation.chat.airoom + +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.user.UserId +import org.chewing.v1.repository.chat.AiChatRoomRepository +import org.springframework.stereotype.Component + +@Component +class AiChatRoomReader( + private val aiChatRoomRepository: AiChatRoomRepository +) { +} diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/message/ChatGenerator.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/message/ChatGenerator.kt index 937e80955..27e8a098b 100755 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/message/ChatGenerator.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/message/ChatGenerator.kt @@ -7,6 +7,7 @@ import org.chewing.v1.model.chat.log.ChatFileLog 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.chewing.v1.model.chat.message.* import org.chewing.v1.model.chat.message.MessageType import org.chewing.v1.model.chat.room.ChatRoomId @@ -192,6 +193,26 @@ class ChatGenerator { ) } + fun generateAiMessage( + chatRoomId: ChatRoomId, + userId: UserId, + roomSequence: ChatRoomSequence, + text: String, + chatRoomType: ChatRoomType, + senderType: SenderType, + ): ChatAiMessage { + return ChatAiMessage.of( + generateKey(chatRoomId), + chatRoomId = chatRoomId, + senderId = userId, + timestamp = LocalDateTime.now(), + roomSequence = roomSequence, + text = text, + chatRoomType = chatRoomType, + senderType = senderType, + ) + } + private fun generateKey(chatRoomId: ChatRoomId): String { return chatRoomId.id + UUID.randomUUID().toString().substring(0, 8) } diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt index fa0c8c114..69162a96b 100755 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt @@ -1,8 +1,11 @@ package org.chewing.v1.implementation.notification +import org.chewing.v1.error.ConflictException +import org.chewing.v1.error.ErrorCode import org.chewing.v1.model.notification.PushInfo import org.chewing.v1.model.chat.message.* import org.chewing.v1.model.chat.room.ChatRoomType +import org.chewing.v1.model.chat.room.ChatRoomType.* import org.chewing.v1.model.friend.FriendShip import org.chewing.v1.model.notification.Notification import org.chewing.v1.model.notification.NotificationInfo @@ -31,15 +34,17 @@ class NotificationGenerator { is ChatFileMessage -> { val content = "사진을 보냈습니다." // val mediaUrl = message.medias.first().url when (message.chatRoomType) { - ChatRoomType.DIRECT -> Triple(NotificationType.DIRECT_CHAT_FILE, message.chatRoomId, content) - ChatRoomType.GROUP -> Triple(NotificationType.GROUP_CHAT_FILE, message.chatRoomId, content) + DIRECT -> Triple(NotificationType.DIRECT_CHAT_FILE, message.chatRoomId, content) + GROUP -> Triple(NotificationType.GROUP_CHAT_FILE, message.chatRoomId, content) + AI -> throw ConflictException(ErrorCode.AI_NOTIFICATION_NOT_SUPPORTED) } } is ChatNormalMessage -> { when (message.chatRoomType) { - ChatRoomType.DIRECT -> Triple(NotificationType.DIRECT_CHAT_NORMAL, message.chatRoomId, message.text) - ChatRoomType.GROUP -> Triple(NotificationType.GROUP_CHAT_NORMAL, message.chatRoomId, message.text) + DIRECT -> Triple(NotificationType.DIRECT_CHAT_NORMAL, message.chatRoomId, message.text) + GROUP -> Triple(NotificationType.GROUP_CHAT_NORMAL, message.chatRoomId, message.text) + AI -> throw ConflictException(ErrorCode.AI_NOTIFICATION_NOT_SUPPORTED) } } @@ -53,8 +58,9 @@ class NotificationGenerator { is ChatReplyMessage -> { when (message.chatRoomType) { - ChatRoomType.DIRECT -> Triple(NotificationType.DIRECT_CHAT_REPLY, message.chatRoomId, message.text) - ChatRoomType.GROUP -> Triple(NotificationType.GROUP_CHAT_REPLY, message.chatRoomId, message.text) + DIRECT -> Triple(NotificationType.DIRECT_CHAT_REPLY, message.chatRoomId, message.text) + GROUP -> Triple(NotificationType.GROUP_CHAT_REPLY, message.chatRoomId, message.text) + AI -> throw ConflictException(ErrorCode.AI_NOTIFICATION_NOT_SUPPORTED) } } diff --git a/domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt b/domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt new file mode 100644 index 000000000..55d78b4ef --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt @@ -0,0 +1,43 @@ +package org.chewing.v1.model.chat.log + +import org.chewing.v1.model.chat.member.SenderType +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.chat.room.ChatRoomSequence +import org.chewing.v1.model.user.UserId +import java.time.LocalDateTime + +class ChatAiLog private constructor( + override val messageId: String, + override val chatRoomId: ChatRoomId, + override val senderId: UserId, + override val timestamp: LocalDateTime, + override val roomSequence: ChatRoomSequence, + override val type: ChatLogType, + val text: String, + val senderType: SenderType +) : ChatLog() { + + companion object { + fun of( + messageId: String, + chatRoomId: ChatRoomId, + senderId: UserId, + text: String, + roomSequence: ChatRoomSequence, + timestamp: LocalDateTime, + type: ChatLogType, + senderType: SenderType + ): ChatAiLog { + return ChatAiLog( + messageId = messageId, + chatRoomId = chatRoomId, + senderId = senderId, + text = text, + roomSequence = roomSequence, + timestamp = timestamp, + type = type, + senderType = senderType + ) + } + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/chat/member/SenderType.kt b/domain/src/main/kotlin/org/chewing/v1/model/chat/member/SenderType.kt new file mode 100644 index 000000000..5021ed33c --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/chat/member/SenderType.kt @@ -0,0 +1,6 @@ +package org.chewing.v1.model.chat.member + +enum class SenderType { + USER, + AI, +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/chat/message/ChatAiMessage.kt b/domain/src/main/kotlin/org/chewing/v1/model/chat/message/ChatAiMessage.kt new file mode 100644 index 000000000..c11b262ce --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/chat/message/ChatAiMessage.kt @@ -0,0 +1,45 @@ +package org.chewing.v1.model.chat.message + +import org.chewing.v1.model.chat.member.SenderType +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 java.time.LocalDateTime + +class ChatAiMessage private constructor( + val messageId: String, + override val chatRoomId: ChatRoomId, + override val senderId: UserId, + override val timestamp: LocalDateTime, + val roomSequence: ChatRoomSequence, + val text: String, + val senderType: SenderType, + override val chatRoomType: ChatRoomType, +) : ChatMessage() { + override val type: MessageType = MessageType.NORMAL + + companion object { + fun of( + messageId: String, + chatRoomId: ChatRoomId, + senderId: UserId, + text: String, + roomSequence: ChatRoomSequence, + timestamp: LocalDateTime, + chatRoomType: ChatRoomType, + senderType: SenderType, + ): ChatAiMessage { + return ChatAiMessage( + messageId = messageId, + chatRoomId = chatRoomId, + senderId = senderId, + text = text, + roomSequence = roomSequence, + timestamp = timestamp, + chatRoomType = chatRoomType, + senderType = senderType, + ) + } + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/chat/room/ChatRoomType.kt b/domain/src/main/kotlin/org/chewing/v1/model/chat/room/ChatRoomType.kt index cdcecd7a2..5ce6d1fd3 100644 --- a/domain/src/main/kotlin/org/chewing/v1/model/chat/room/ChatRoomType.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/chat/room/ChatRoomType.kt @@ -3,4 +3,5 @@ package org.chewing.v1.model.chat.room enum class ChatRoomType { GROUP, DIRECT, + AI, } diff --git a/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt b/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt new file mode 100644 index 000000000..daf645084 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt @@ -0,0 +1,8 @@ +package org.chewing.v1.repository.chat + +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.user.UserId + +interface AiChatRoomRepository { + fun append(userId: UserId): ChatRoomId +} diff --git a/domain/src/main/kotlin/org/chewing/v1/service/ai/AiService.kt b/domain/src/main/kotlin/org/chewing/v1/service/ai/AiService.kt new file mode 100644 index 000000000..4023070b0 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/service/ai/AiService.kt @@ -0,0 +1,14 @@ +package org.chewing.v1.service.ai + +import org.chewing.v1.implementation.ai.AiSender +import org.chewing.v1.model.ai.Prompt +import org.springframework.stereotype.Service + +@Service +class AiService ( + private val aiSender: AiSender +){ + fun prompt(prompts: List): String { + return aiSender.sendPrompt(prompts) + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt b/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt new file mode 100644 index 000000000..d6a827db1 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt @@ -0,0 +1,18 @@ +package org.chewing.v1.service.chat + +import org.chewing.v1.implementation.chat.airoom.AiChatRoomAppender +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.user.UserId +import org.chewing.v1.repository.chat.AiChatRoomRepository +import org.springframework.stereotype.Service + +@Service +class AiChatRoomService( + private val aiChatRoomAppender: AiChatRoomAppender, +) { + fun createAiChatRoom( + userId: UserId, + ): ChatRoomId { + return aiChatRoomAppender.appendChatRoom(userId) + } +} 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 28d682e82..edf3d9fd2 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 @@ -9,6 +9,7 @@ import org.chewing.v1.implementation.chat.message.ChatValidator import org.chewing.v1.implementation.media.FileHandler import org.chewing.v1.model.chat.log.ChatLog import org.chewing.v1.model.chat.log.UnReadTarget +import org.chewing.v1.model.chat.member.SenderType import org.chewing.v1.model.chat.message.* import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.chat.room.ChatRoomType @@ -154,6 +155,19 @@ class ChatLogService( return chatMessage } + fun aiMessage( + chatRoomId: ChatRoomId, + userId: UserId, + roomSequence: ChatRoomSequence, + text: String, + chatRoomType: ChatRoomType, + senderType: SenderType, + ): ChatAiMessage { + val chatMessage = chatGenerator.generateAiMessage(chatRoomId, userId, roomSequence, text, chatRoomType, senderType) + chatAppender.appendChatLog(chatMessage) + return chatMessage + } + fun getsLatestChatLog(chatRoomIds: List): List { return chatReader.readLatestMessages(chatRoomIds) } diff --git a/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt b/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt index f575ce530..c1747236a 100755 --- a/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt +++ b/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt @@ -3,6 +3,7 @@ package org.chewing.v1.external import org.chewing.v1.dto.ChatMessageDto import org.chewing.v1.model.chat.message.ChatMessage import org.chewing.v1.model.chat.room.ChatRoomType +import org.chewing.v1.model.chat.room.ChatRoomType.* import org.chewing.v1.model.user.UserId import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.stereotype.Component @@ -13,20 +14,22 @@ class ExternalChatNotificationClientImpl( ) : ExternalChatNotificationClient { override fun sendMessage(chatMessage: ChatMessage, userId: UserId) { when (chatMessage.chatRoomType) { - ChatRoomType.GROUP -> { + GROUP -> { messagingTemplate.convertAndSendToUser( userId.id, "/queue/chat/group", ChatMessageDto.from(chatMessage), ) } - ChatRoomType.DIRECT -> { + DIRECT -> { messagingTemplate.convertAndSendToUser( userId.id, "/queue/chat/direct", ChatMessageDto.from(chatMessage), ) } + + AI -> null } } } diff --git a/storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatAiMongoEntity.kt b/storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatAiMongoEntity.kt new file mode 100644 index 000000000..35284340d --- /dev/null +++ b/storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatAiMongoEntity.kt @@ -0,0 +1,60 @@ +package org.chewing.v1.mongoentity + +import org.chewing.v1.model.chat.log.ChatAiLog +import org.chewing.v1.model.chat.log.ChatLog +import org.chewing.v1.model.chat.message.ChatAiMessage + +import org.chewing.v1.model.chat.log.ChatLogType +import org.chewing.v1.model.chat.member.SenderType +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.chat.room.ChatRoomSequence +import org.chewing.v1.model.user.UserId +import org.springframework.data.mongodb.core.mapping.Document +import java.time.LocalDateTime + +@Document(collection = "chat_messages") +internal class ChatAiMongoEntity( + messageId: String, + chatRoomId: String, + senderId: String, + sequence: Int, + createAt: LocalDateTime, + private val message: String, + private val senderType: SenderType, +) : ChatMessageMongoEntity( + messageId = messageId, + chatRoomId = chatRoomId, + type = ChatLogType.NORMAL, + senderId = senderId, + sequence = sequence, + createAt = createAt, +) { + companion object { + fun from( + chatAiMessage: ChatAiMessage, + ): ChatAiMongoEntity { + return ChatAiMongoEntity( + messageId = chatAiMessage.messageId, + chatRoomId = chatAiMessage.chatRoomId.id, + senderId = chatAiMessage.senderId.id, + sequence = chatAiMessage.roomSequence.sequence, + createAt = chatAiMessage.timestamp, + message = chatAiMessage.text, + senderType = chatAiMessage.senderType, + ) + } + } + + override fun toChatLog(): ChatLog { + return ChatAiLog.of( + messageId = messageId, + chatRoomId = ChatRoomId.of(chatRoomId), + senderId = UserId.of(senderId), + timestamp = this@ChatAiMongoEntity.createAt, + roomSequence = ChatRoomSequence.of(ChatRoomId.of(chatRoomId), this@ChatAiMongoEntity.sequence), + text = message, + type = type, + senderType = senderType, + ) + } +} diff --git a/storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatMessageMongoEntity.kt b/storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatMessageMongoEntity.kt index 54cb50d82..869ff7462 100755 --- a/storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatMessageMongoEntity.kt +++ b/storage/src/main/kotlin/org/chewing/v1/mongoentity/ChatMessageMongoEntity.kt @@ -45,6 +45,7 @@ internal sealed class ChatMessageMongoEntity( is ChatReplyMessage -> ChatReplyMongoEntity.from(chatMessage) is ChatErrorMessage -> null is ChatCommentMessage -> ChatCommentMongoEntity.from(chatMessage) + is ChatAiMessage -> ChatAiMongoEntity.from(chatMessage) } } From c194a592697bb227a0891775111576d976379fc2 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Wed, 23 Apr 2025 15:36:36 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20userRole=20=EC=B6=94=EA=B0=80,=20ai?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B4=80=EB=A0=A8=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=B6=84=EA=B8=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/ThumbnailGroupChatRoomResponse.kt | 5 +++++ api/src/main/resources/application.yml | 1 + .../test/kotlin/org/chewing/v1/TestDataFactory.kt | 2 ++ .../chewing/v1/controller/MainControllerTest.kt | 3 +++ .../main/kotlin/org/chewing/v1/error/ErrorCode.kt | 2 ++ .../v1/implementation/auth/AuthGenerator.kt | 3 ++- .../kotlin/org/chewing/v1/model/user/UserInfo.kt | 3 +++ .../kotlin/org/chewing/v1/model/user/UserRole.kt | 6 ++++++ .../test/kotlin/org/chewing/v1/TestDataFactory.kt | 2 ++ .../kotlin/org/chewing/v1/dto/ChatMessageDto.kt | 4 ++++ .../test/kotlin/org/chewing/v1/TestDataFactory.kt | 1 + .../org/chewing/v1/jpaentity/user/UserJpaEntity.kt | 6 ++++++ .../jpa/chat/AiChatRoomRepositoryImpl.kt | 14 ++++++++++++++ .../chewing/v1/repository/support/UserProvider.kt | 2 ++ 14 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/user/UserRole.kt create mode 100644 storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt diff --git a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ThumbnailGroupChatRoomResponse.kt b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ThumbnailGroupChatRoomResponse.kt index ae4eb4c1d..2e0c48b25 100644 --- a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ThumbnailGroupChatRoomResponse.kt +++ b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/ThumbnailGroupChatRoomResponse.kt @@ -1,5 +1,8 @@ package org.chewing.v1.dto.response.chat +import org.chewing.v1.error.ConflictException +import org.chewing.v1.error.ErrorCode +import org.chewing.v1.model.chat.log.ChatAiLog import org.chewing.v1.model.chat.log.ChatCommentLog import org.chewing.v1.model.chat.log.ChatFileLog import org.chewing.v1.model.chat.log.ChatInviteLog @@ -94,6 +97,8 @@ data class ThumbnailGroupChatRoomResponse( chatRoomSequenceNumber = chatRoom.roomSequence.sequence, chatRoomName = chatRoom.roomInfo.name, ) + + is ChatAiLog -> throw ConflictException(ErrorCode.AI_NOT_SUPPORTED) } } } diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 43c193e51..064f9a533 100755 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -12,6 +12,7 @@ spring: - fcm.yml - ncp.yml - media.yml + - openai.yml - optional:file:../.env[.properties] - optional:file:./.env[.properties] mvc: diff --git a/api/src/test/kotlin/org/chewing/v1/TestDataFactory.kt b/api/src/test/kotlin/org/chewing/v1/TestDataFactory.kt index 387bc081c..6dd346081 100755 --- a/api/src/test/kotlin/org/chewing/v1/TestDataFactory.kt +++ b/api/src/test/kotlin/org/chewing/v1/TestDataFactory.kt @@ -62,6 +62,7 @@ import org.chewing.v1.model.user.AccessStatus import org.chewing.v1.model.user.User import org.chewing.v1.model.user.UserInfo import org.chewing.v1.model.user.UserId +import org.chewing.v1.model.user.UserRole import org.springframework.mock.web.MockMultipartFile import java.awt.Color import java.awt.image.BufferedImage @@ -220,6 +221,7 @@ object TestDataFactory { "testPassword", "testStatusMessage", LocalDate.now(), + UserRole.USER, ) } diff --git a/api/src/test/kotlin/org/chewing/v1/controller/MainControllerTest.kt b/api/src/test/kotlin/org/chewing/v1/controller/MainControllerTest.kt index 564761c91..701d7ac76 100755 --- a/api/src/test/kotlin/org/chewing/v1/controller/MainControllerTest.kt +++ b/api/src/test/kotlin/org/chewing/v1/controller/MainControllerTest.kt @@ -13,6 +13,7 @@ import org.chewing.v1.facade.DirectChatFacade import org.chewing.v1.facade.FriendFacade import org.chewing.v1.facade.FriendFeedFacade import org.chewing.v1.facade.GroupChatFacade +import org.chewing.v1.model.chat.log.ChatAiLog import org.chewing.v1.model.chat.log.ChatCommentLog import org.chewing.v1.model.chat.log.ChatFileLog import org.chewing.v1.model.chat.log.ChatInviteLog @@ -239,6 +240,8 @@ class MainControllerTest : RestDocsTest() { body("$mediaPath.index", equalTo(media.index)) } } + + is ChatAiLog -> null } } } diff --git a/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt b/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt index 789e9f4db..db620961c 100755 --- a/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt +++ b/common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt @@ -80,6 +80,8 @@ enum class ErrorCode( AI_PROMPT_FAILED("AI_1", "AI 프롬프트를 실패하였습니다."), AI_NOTIFICATION_NOT_SUPPORTED("AI_2", "AI 알림을 지원하지 않습니다."), + AI_WEBSOCKET_NOT_SUPPORTED("AI_3", "AI 웹소켓을 지원하지 않습니다."), + AI_NOT_SUPPORTED("AI_4", "AI를 지원하지 않습니다."), ; companion object { diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt index ef16d90f0..6b9879b14 100755 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt @@ -8,7 +8,8 @@ import java.util.Random class AuthGenerator { fun generateVerificationCode(): String { - return (100000 + Random().nextInt(900000)).toString() +// return (100000 + Random().nextInt(900000)).toString() + return "000000" } fun hashPassword(password: String): String { diff --git a/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt b/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt index 420e94ec7..67c20187d 100755 --- a/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt @@ -13,6 +13,7 @@ class UserInfo private constructor( val password: String, val statusMessage: String, val birthday: LocalDate?, + val role: UserRole ) { companion object { fun of( @@ -24,6 +25,7 @@ class UserInfo private constructor( password: String, statusMessage: String, birthday: LocalDate?, + role: UserRole, ): UserInfo { return UserInfo( userId = userId, @@ -34,6 +36,7 @@ class UserInfo private constructor( password = password, statusMessage = statusMessage, birthday = birthday, + role = role, ) } } diff --git a/domain/src/main/kotlin/org/chewing/v1/model/user/UserRole.kt b/domain/src/main/kotlin/org/chewing/v1/model/user/UserRole.kt new file mode 100644 index 000000000..f20f556da --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/user/UserRole.kt @@ -0,0 +1,6 @@ +package org.chewing.v1.model.user + +enum class UserRole { + USER, + ADMIN, +} diff --git a/domain/src/test/kotlin/org/chewing/v1/TestDataFactory.kt b/domain/src/test/kotlin/org/chewing/v1/TestDataFactory.kt index 46e32121e..20b7e6869 100755 --- a/domain/src/test/kotlin/org/chewing/v1/TestDataFactory.kt +++ b/domain/src/test/kotlin/org/chewing/v1/TestDataFactory.kt @@ -167,6 +167,7 @@ object TestDataFactory { "testPassword", "testStatusMessage", LocalDate.now(), + UserRole.USER, ) } @@ -184,6 +185,7 @@ object TestDataFactory { password, "testStatusMessage", LocalDate.now(), + UserRole.USER, ) fun createScheduledTime(): ScheduleTime = diff --git a/external/src/main/kotlin/org/chewing/v1/dto/ChatMessageDto.kt b/external/src/main/kotlin/org/chewing/v1/dto/ChatMessageDto.kt index 4ff51a378..924523227 100755 --- a/external/src/main/kotlin/org/chewing/v1/dto/ChatMessageDto.kt +++ b/external/src/main/kotlin/org/chewing/v1/dto/ChatMessageDto.kt @@ -2,6 +2,8 @@ package org.chewing.v1.dto import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo +import org.chewing.v1.error.ConflictException +import org.chewing.v1.error.ErrorCode import org.chewing.v1.model.chat.message.* import java.time.format.DateTimeFormatter @@ -225,6 +227,8 @@ sealed class ChatMessageDto { comment = chatMessage.comment, content = chatMessage.content, ) + + is ChatAiMessage -> throw ConflictException(ErrorCode.AI_WEBSOCKET_NOT_SUPPORTED) } } } diff --git a/external/src/test/kotlin/org/chewing/v1/TestDataFactory.kt b/external/src/test/kotlin/org/chewing/v1/TestDataFactory.kt index d429680f8..8bb946ebd 100755 --- a/external/src/test/kotlin/org/chewing/v1/TestDataFactory.kt +++ b/external/src/test/kotlin/org/chewing/v1/TestDataFactory.kt @@ -30,6 +30,7 @@ object TestDataFactory { "testPassword", "testStatusMessage", LocalDate.now(), + UserRole.USER, ) } diff --git a/storage/src/main/kotlin/org/chewing/v1/jpaentity/user/UserJpaEntity.kt b/storage/src/main/kotlin/org/chewing/v1/jpaentity/user/UserJpaEntity.kt index 02a46834b..e1964bc16 100755 --- a/storage/src/main/kotlin/org/chewing/v1/jpaentity/user/UserJpaEntity.kt +++ b/storage/src/main/kotlin/org/chewing/v1/jpaentity/user/UserJpaEntity.kt @@ -9,6 +9,7 @@ import org.chewing.v1.model.media.MediaType import org.chewing.v1.model.user.AccessStatus import org.chewing.v1.model.user.UserInfo import org.chewing.v1.model.user.UserId +import org.chewing.v1.model.user.UserRole import org.hibernate.annotations.DynamicInsert import java.time.LocalDate import java.util.* @@ -49,6 +50,9 @@ internal class UserJpaEntity( private var statusMessage: String, private var birthday: LocalDate?, + + @Enumerated(EnumType.STRING) + private val role: UserRole, ) : BaseEntity() { companion object { fun generate(phoneNumber: PhoneNumber, userName: String, access: AccessStatus): UserJpaEntity { @@ -63,6 +67,7 @@ internal class UserJpaEntity( password = "", statusMessage = "", birthday = null, + role = UserRole.USER, ) } } @@ -77,6 +82,7 @@ internal class UserJpaEntity( this.password, this.statusMessage, this.birthday, + this.role, ) } diff --git a/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt new file mode 100644 index 000000000..84a17999d --- /dev/null +++ b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt @@ -0,0 +1,14 @@ +package org.chewing.v1.repository.jpa.chat + +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.user.UserId +import org.chewing.v1.repository.chat.AiChatRoomRepository +import org.springframework.stereotype.Repository + + +@Repository +internal class AiChatRoomRepositoryImpl(): AiChatRoomRepository { + override fun append(userId: UserId): ChatRoomId { + TODO("Not yet implemented") + } +} diff --git a/storage/src/test/kotlin/org/chewing/v1/repository/support/UserProvider.kt b/storage/src/test/kotlin/org/chewing/v1/repository/support/UserProvider.kt index 2e5394590..79cc38937 100755 --- a/storage/src/test/kotlin/org/chewing/v1/repository/support/UserProvider.kt +++ b/storage/src/test/kotlin/org/chewing/v1/repository/support/UserProvider.kt @@ -6,6 +6,7 @@ import org.chewing.v1.model.media.MediaType import org.chewing.v1.model.user.AccessStatus import org.chewing.v1.model.user.UserInfo import org.chewing.v1.model.user.UserId +import org.chewing.v1.model.user.UserRole import java.time.LocalDate object UserProvider { @@ -23,6 +24,7 @@ object UserProvider { "testPassword", "testStatusMessage", LocalDate.now(), + UserRole.USER, ) } From 8915665c5d38426816aaaeb6e6b186155585b5ed Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Fri, 25 Apr 2025 18:58:18 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20AI=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/chewing/v1/facade/AiFacade.kt | 32 +++++++++++++++++++ .../chat/airoom/AiChatRoomReader.kt | 6 ++++ .../v1/model/chat/room/AiChatRoomInfo.kt | 23 +++++++++++++ .../repository/chat/AiChatRoomRepository.kt | 2 ++ .../ai/{AiService.kt => AiPromptService.kt} | 5 +-- .../v1/service/chat/AiChatRoomService.kt | 21 +++++++++++- .../chewing/v1/service/chat/ChatLogService.kt | 3 +- 7 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/chat/room/AiChatRoomInfo.kt rename domain/src/main/kotlin/org/chewing/v1/service/ai/{AiService.kt => AiPromptService.kt} (69%) diff --git a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt new file mode 100644 index 000000000..4bc037e3e --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -0,0 +1,32 @@ +package org.chewing.v1.facade + +import org.chewing.v1.model.chat.member.SenderType +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.chat.room.ChatRoomMemberStatus +import org.chewing.v1.model.chat.room.ChatRoomType +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.springframework.stereotype.Service + +@Service +class AiFacade( + private val aiChatRoomService: AiChatRoomService, + private val aiPromptService: AiPromptService, + private val chatLogService: ChatLogService, +) { + fun processAiMessage( + userId: UserId, + chatRoomId: ChatRoomId, + text: String, + ): String { + val directChatRoom = aiChatRoomService.getAiChatRoom(chatRoomId, userId) + val chatSequence = aiChatRoomService.increaseDirectChatRoomSequence(directChatRoom.chatRoomId) + chatLogService.aiMessage(directChatRoom.chatRoomId, userId, chatSequence, text, ChatRoomType.AI, SenderType.USER) + val chatLogs = chatLogService.getChatLogs(directChatRoom.chatRoomId, chatSequence.sequence, 0) + val aiMessage = aiPromptService.prompt(chatLogs) + chatLogService.aiMessage(directChatRoom.chatRoomId, userId, chatSequence, aiMessage, ChatRoomType.AI, SenderType.AI) + return aiMessage + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt index c746a868e..0a8b12062 100644 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt @@ -1,5 +1,7 @@ package org.chewing.v1.implementation.chat.airoom +import org.chewing.v1.error.ErrorCode +import org.chewing.v1.error.NotFoundException import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.user.UserId import org.chewing.v1.repository.chat.AiChatRoomRepository @@ -9,4 +11,8 @@ import org.springframework.stereotype.Component class AiChatRoomReader( private val aiChatRoomRepository: AiChatRoomRepository ) { + fun readRoomInfo(chatRoomId: ChatRoomId, userId: UserId) = + aiChatRoomRepository.readInfo(chatRoomId, userId) ?: throw NotFoundException( + ErrorCode.CHATROOM_NOT_FOUND, + ) } diff --git a/domain/src/main/kotlin/org/chewing/v1/model/chat/room/AiChatRoomInfo.kt b/domain/src/main/kotlin/org/chewing/v1/model/chat/room/AiChatRoomInfo.kt new file mode 100644 index 000000000..5fb390d57 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/chat/room/AiChatRoomInfo.kt @@ -0,0 +1,23 @@ +package org.chewing.v1.model.chat.room + +import org.chewing.v1.model.user.UserId + +class AiChatRoomInfo private constructor( + val chatRoomId: ChatRoomId, + val userId: UserId, + val status: ChatRoomMemberStatus, +) { + companion object { + fun of( + chatRoomId: ChatRoomId, + userId: UserId, + status: ChatRoomMemberStatus, + ): AiChatRoomInfo { + return AiChatRoomInfo( + chatRoomId = chatRoomId, + userId = userId, + status = status, + ) + } + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt b/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt index daf645084..ef2cf53de 100644 --- a/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt +++ b/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt @@ -1,8 +1,10 @@ package org.chewing.v1.repository.chat +import org.chewing.v1.model.chat.room.AiChatRoomInfo import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.user.UserId interface AiChatRoomRepository { fun append(userId: UserId): ChatRoomId + fun readInfo(chatRoomId: ChatRoomId,userId: UserId): AiChatRoomInfo? } diff --git a/domain/src/main/kotlin/org/chewing/v1/service/ai/AiService.kt b/domain/src/main/kotlin/org/chewing/v1/service/ai/AiPromptService.kt similarity index 69% rename from domain/src/main/kotlin/org/chewing/v1/service/ai/AiService.kt rename to domain/src/main/kotlin/org/chewing/v1/service/ai/AiPromptService.kt index 4023070b0..6a04f7f0d 100644 --- a/domain/src/main/kotlin/org/chewing/v1/service/ai/AiService.kt +++ b/domain/src/main/kotlin/org/chewing/v1/service/ai/AiPromptService.kt @@ -2,13 +2,14 @@ package org.chewing.v1.service.ai import org.chewing.v1.implementation.ai.AiSender import org.chewing.v1.model.ai.Prompt +import org.chewing.v1.model.chat.log.ChatLog import org.springframework.stereotype.Service @Service -class AiService ( +class AiPromptService ( private val aiSender: AiSender ){ - fun prompt(prompts: List): String { + fun prompt(chatlogs: List): String { return aiSender.sendPrompt(prompts) } } diff --git a/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt b/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt index d6a827db1..c9047f2a9 100644 --- a/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt +++ b/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt @@ -1,7 +1,11 @@ package org.chewing.v1.service.chat import org.chewing.v1.implementation.chat.airoom.AiChatRoomAppender +import org.chewing.v1.implementation.chat.airoom.AiChatRoomReader +import org.chewing.v1.implementation.chat.sequence.ChatSequenceHandler +import org.chewing.v1.model.chat.room.AiChatRoomInfo import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.chat.room.ChatRoomSequence import org.chewing.v1.model.user.UserId import org.chewing.v1.repository.chat.AiChatRoomRepository import org.springframework.stereotype.Service @@ -9,10 +13,25 @@ import org.springframework.stereotype.Service @Service class AiChatRoomService( private val aiChatRoomAppender: AiChatRoomAppender, + private val aiChatRoomReader: AiChatRoomReader, + private val chatSequenceHandler: ChatSequenceHandler, ) { fun createAiChatRoom( userId: UserId, ): ChatRoomId { - return aiChatRoomAppender.appendChatRoom(userId) + val chatRoomId = aiChatRoomAppender.appendChatRoom(userId) + chatSequenceHandler.handleCreateRoomSequence(chatRoomId) + chatSequenceHandler.handleCreateMemberSequences(chatRoomId, listOf(userId)) + return chatRoomId + } + fun getAiChatRoom( + chatRoomId: ChatRoomId, + userId: UserId, + ): AiChatRoomInfo { + return aiChatRoomReader.readRoomInfo(chatRoomId,userId) + } + + fun increaseDirectChatRoomSequence(chatRoomId: ChatRoomId): ChatRoomSequence { + return chatSequenceHandler.handleRoomIncreaseSequence(chatRoomId) } } 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..0a2728691 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 @@ -162,10 +162,9 @@ class ChatLogService( text: String, chatRoomType: ChatRoomType, senderType: SenderType, - ): ChatAiMessage { + ) { val chatMessage = chatGenerator.generateAiMessage(chatRoomId, userId, roomSequence, text, chatRoomType, senderType) chatAppender.appendChatLog(chatMessage) - return chatMessage } fun getsLatestChatLog(chatRoomIds: List): List { From 8f91aced44c4d0083fd656d835010bd3b077bad4 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Sat, 26 Apr 2025 21:59:06 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20AIChatRoom=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chewing/v1/controller/ai/AiController.kt | 27 ++++++++++++++++ .../kotlin/org/chewing/v1/facade/AiFacade.kt | 17 ++++++---- .../v1/implementation/ai/AiPromptGenerator.kt | 32 +++++++++++++++++++ .../v1/implementation/auth/AuthGenerator.kt | 1 - .../chat/airoom/AiChatRoomReader.kt | 2 +- .../chat/sequence/ChatSequenceHandler.kt | 4 +++ .../notification/NotificationGenerator.kt | 1 - .../chewing/v1/model/chat/log/ChatAiLog.kt | 6 ++-- .../org/chewing/v1/model/user/UserInfo.kt | 2 +- .../repository/chat/AiChatRoomRepository.kt | 2 +- .../chewing/v1/service/ai/AiPromptService.kt | 10 +++--- .../v1/service/chat/AiChatRoomService.kt | 5 ++- .../ExternalChatNotificationClientImpl.kt | 1 - .../jpa/chat/AiChatRoomRepositoryImpl.kt | 11 +++++-- 14 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiPromptGenerator.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 new file mode 100644 index 000000000..117943dc2 --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt @@ -0,0 +1,27 @@ +package org.chewing.v1.controller.ai + +import org.chewing.v1.dto.response.chat.ChatRoomListResponse +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.GetMapping +import org.springframework.web.bind.annotation.PostMapping + +@Controller +class AiController( + private val aiFacade: AiFacade, + private val aiChatRoomService: AiChatRoomService +) { + @PostMapping("/ai/chat/room") + fun createAiChatRoom( + @CurrentUser userId: UserId, + ): SuccessResponseEntity { + val chatRoomId = aiChatRoomService.createAiChatRoom(userId) + return ResponseHelper.success(chatRoomId) + } +} 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 4bc037e3e..8a540d11e 100644 --- a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -2,7 +2,6 @@ package org.chewing.v1.facade import org.chewing.v1.model.chat.member.SenderType import org.chewing.v1.model.chat.room.ChatRoomId -import org.chewing.v1.model.chat.room.ChatRoomMemberStatus import org.chewing.v1.model.chat.room.ChatRoomType import org.chewing.v1.model.user.UserId import org.chewing.v1.service.ai.AiPromptService @@ -21,12 +20,18 @@ class AiFacade( chatRoomId: ChatRoomId, text: String, ): String { - val directChatRoom = aiChatRoomService.getAiChatRoom(chatRoomId, userId) - val chatSequence = aiChatRoomService.increaseDirectChatRoomSequence(directChatRoom.chatRoomId) - chatLogService.aiMessage(directChatRoom.chatRoomId, userId, chatSequence, text, ChatRoomType.AI, SenderType.USER) - val chatLogs = chatLogService.getChatLogs(directChatRoom.chatRoomId, chatSequence.sequence, 0) + val aiChatRoom = aiChatRoomService.getAiChatRoom(chatRoomId, userId) + val chatSequence = aiChatRoomService.increaseDirectChatRoomSequence(aiChatRoom.chatRoomId) + chatLogService.aiMessage(aiChatRoom.chatRoomId, userId, chatSequence, text, ChatRoomType.AI, SenderType.USER) + val chatLogs = chatLogService.getChatLogs(aiChatRoom.chatRoomId, chatSequence.sequence, 0) val aiMessage = aiPromptService.prompt(chatLogs) - chatLogService.aiMessage(directChatRoom.chatRoomId, userId, chatSequence, aiMessage, ChatRoomType.AI, SenderType.AI) + chatLogService.aiMessage(aiChatRoom.chatRoomId, userId, chatSequence, aiMessage, ChatRoomType.AI, SenderType.AI) return aiMessage } + + fun produceAiChatRoom( + userId: UserId, + ): ChatRoomId { + return aiChatRoomService.createAiChatRoom(userId) + } } 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 new file mode 100644 index 000000000..34432baa9 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiPromptGenerator.kt @@ -0,0 +1,32 @@ +package org.chewing.v1.implementation.ai + +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.TextPrompt +import org.chewing.v1.model.chat.log.ChatAiLog +import org.chewing.v1.model.chat.log.ChatLog +import org.chewing.v1.model.chat.member.SenderType +import org.springframework.stereotype.Component + +@Component +class AiPromptGenerator { + + fun generateChatLogPrompts(chatLogs: List): List { + return chatLogs.map { chatLog -> + + if (chatLog !is ChatAiLog) { + throw ConflictException(ErrorCode.AI_NOT_SUPPORTED) + } + + TextPrompt.of( + role = when (chatLog.senderType) { + SenderType.AI -> PromptRole.ASSISTANT + SenderType.USER -> PromptRole.USER + }, + text = chatLog.text, + ) + } + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt index 6b9879b14..d890621bf 100755 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/auth/AuthGenerator.kt @@ -2,7 +2,6 @@ package org.chewing.v1.implementation.auth import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.stereotype.Component -import java.util.Random @Component class AuthGenerator { diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt index 0a8b12062..b6f1f05a1 100644 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/airoom/AiChatRoomReader.kt @@ -9,7 +9,7 @@ import org.springframework.stereotype.Component @Component class AiChatRoomReader( - private val aiChatRoomRepository: AiChatRoomRepository + private val aiChatRoomRepository: AiChatRoomRepository, ) { fun readRoomInfo(chatRoomId: ChatRoomId, userId: UserId) = aiChatRoomRepository.readInfo(chatRoomId, userId) ?: throw NotFoundException( diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/sequence/ChatSequenceHandler.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/sequence/ChatSequenceHandler.kt index dc63172f7..1a94a86ff 100644 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/chat/sequence/ChatSequenceHandler.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/chat/sequence/ChatSequenceHandler.kt @@ -32,6 +32,10 @@ class ChatSequenceHandler( } } + fun handleCreateRoomSequence(chatRoomId: ChatRoomId, userId: UserId) { + chatRoomMemberSequenceRepository.appendSequence(chatRoomId, userId) + } + fun handleCreateRoomSequence(chatRoomId: ChatRoomId) { chatRoomSequenceRepository.appendSequence(chatRoomId) } diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt index 69162a96b..7056b635b 100755 --- a/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/notification/NotificationGenerator.kt @@ -4,7 +4,6 @@ import org.chewing.v1.error.ConflictException import org.chewing.v1.error.ErrorCode import org.chewing.v1.model.notification.PushInfo import org.chewing.v1.model.chat.message.* -import org.chewing.v1.model.chat.room.ChatRoomType import org.chewing.v1.model.chat.room.ChatRoomType.* import org.chewing.v1.model.friend.FriendShip import org.chewing.v1.model.notification.Notification diff --git a/domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt b/domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt index 55d78b4ef..cd1e4ae45 100644 --- a/domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/chat/log/ChatAiLog.kt @@ -14,7 +14,7 @@ class ChatAiLog private constructor( override val roomSequence: ChatRoomSequence, override val type: ChatLogType, val text: String, - val senderType: SenderType + val senderType: SenderType, ) : ChatLog() { companion object { @@ -26,7 +26,7 @@ class ChatAiLog private constructor( roomSequence: ChatRoomSequence, timestamp: LocalDateTime, type: ChatLogType, - senderType: SenderType + senderType: SenderType, ): ChatAiLog { return ChatAiLog( messageId = messageId, @@ -36,7 +36,7 @@ class ChatAiLog private constructor( roomSequence = roomSequence, timestamp = timestamp, type = type, - senderType = senderType + senderType = senderType, ) } } diff --git a/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt b/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt index 67c20187d..7b7f79287 100755 --- a/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt +++ b/domain/src/main/kotlin/org/chewing/v1/model/user/UserInfo.kt @@ -13,7 +13,7 @@ class UserInfo private constructor( val password: String, val statusMessage: String, val birthday: LocalDate?, - val role: UserRole + val role: UserRole, ) { companion object { fun of( diff --git a/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt b/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt index ef2cf53de..3139fb990 100644 --- a/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt +++ b/domain/src/main/kotlin/org/chewing/v1/repository/chat/AiChatRoomRepository.kt @@ -6,5 +6,5 @@ import org.chewing.v1.model.user.UserId interface AiChatRoomRepository { fun append(userId: UserId): ChatRoomId - fun readInfo(chatRoomId: ChatRoomId,userId: UserId): AiChatRoomInfo? + fun readInfo(chatRoomId: ChatRoomId, userId: UserId): AiChatRoomInfo? } 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 6a04f7f0d..6afb8b6ec 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 @@ -1,15 +1,17 @@ package org.chewing.v1.service.ai +import org.chewing.v1.implementation.ai.AiPromptGenerator import org.chewing.v1.implementation.ai.AiSender -import org.chewing.v1.model.ai.Prompt import org.chewing.v1.model.chat.log.ChatLog import org.springframework.stereotype.Service @Service -class AiPromptService ( - private val aiSender: AiSender -){ +class AiPromptService( + private val aiSender: AiSender, + private val aiPromptGenerator: AiPromptGenerator, +) { fun prompt(chatlogs: List): String { + val prompts = aiPromptGenerator.generateChatLogPrompts(chatlogs) return aiSender.sendPrompt(prompts) } } diff --git a/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt b/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt index c9047f2a9..06fe56beb 100644 --- a/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt +++ b/domain/src/main/kotlin/org/chewing/v1/service/chat/AiChatRoomService.kt @@ -7,7 +7,6 @@ import org.chewing.v1.model.chat.room.AiChatRoomInfo import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.chat.room.ChatRoomSequence import org.chewing.v1.model.user.UserId -import org.chewing.v1.repository.chat.AiChatRoomRepository import org.springframework.stereotype.Service @Service @@ -21,14 +20,14 @@ class AiChatRoomService( ): ChatRoomId { val chatRoomId = aiChatRoomAppender.appendChatRoom(userId) chatSequenceHandler.handleCreateRoomSequence(chatRoomId) - chatSequenceHandler.handleCreateMemberSequences(chatRoomId, listOf(userId)) + chatSequenceHandler.handleCreateRoomSequence(chatRoomId, userId) return chatRoomId } fun getAiChatRoom( chatRoomId: ChatRoomId, userId: UserId, ): AiChatRoomInfo { - return aiChatRoomReader.readRoomInfo(chatRoomId,userId) + return aiChatRoomReader.readRoomInfo(chatRoomId, userId) } fun increaseDirectChatRoomSequence(chatRoomId: ChatRoomId): ChatRoomSequence { diff --git a/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt b/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt index c1747236a..814ccaf33 100755 --- a/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt +++ b/external/src/main/kotlin/org/chewing/v1/external/ExternalChatNotificationClientImpl.kt @@ -2,7 +2,6 @@ package org.chewing.v1.external import org.chewing.v1.dto.ChatMessageDto import org.chewing.v1.model.chat.message.ChatMessage -import org.chewing.v1.model.chat.room.ChatRoomType import org.chewing.v1.model.chat.room.ChatRoomType.* import org.chewing.v1.model.user.UserId import org.springframework.messaging.simp.SimpMessagingTemplate diff --git a/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt index 84a17999d..3a4fbb100 100644 --- a/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt +++ b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt @@ -1,14 +1,21 @@ package org.chewing.v1.repository.jpa.chat +import org.chewing.v1.model.chat.room.AiChatRoomInfo import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.user.UserId import org.chewing.v1.repository.chat.AiChatRoomRepository import org.springframework.stereotype.Repository - @Repository -internal class AiChatRoomRepositoryImpl(): AiChatRoomRepository { +internal class AiChatRoomRepositoryImpl() : AiChatRoomRepository { override fun append(userId: UserId): ChatRoomId { TODO("Not yet implemented") } + + override fun readInfo( + chatRoomId: ChatRoomId, + userId: UserId, + ): AiChatRoomInfo? { + TODO("Not yet implemented") + } } From 30755977b6a5a1506aed0c95c45507c7bc91b045 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Sun, 27 Apr 2025 23:24:14 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20ai=20=EA=B4=80=EB=A0=A8=20userId=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20Controller=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chewing/v1/controller/ai/AiController.kt | 16 ++++++++--- .../response/chat/AiChatMessageResponse.kt | 27 +++++++++++++++++++ api/src/main/resources/application.yml | 1 + .../kotlin/org/chewing/v1/facade/AiFacade.kt | 12 ++++++--- .../v1/implementation/ai/AiUserGenerator.kt | 15 +++++++++++ .../chewing/v1/service/chat/ChatLogService.kt | 3 ++- domain/src/main/resources/ai.yml | 12 +++++++++ 7 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiUserGenerator.kt create mode 100644 domain/src/main/resources/ai.yml 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 117943dc2..7a666d7cd 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.response.chat.ChatRoomListResponse +import org.chewing.v1.dto.request.chat.ChatRequest +import org.chewing.v1.dto.response.chat.AiChatMessageResponse import org.chewing.v1.facade.AiFacade import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.user.UserId @@ -9,8 +10,8 @@ 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.GetMapping import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody @Controller class AiController( @@ -21,7 +22,16 @@ class AiController( fun createAiChatRoom( @CurrentUser userId: UserId, ): SuccessResponseEntity { - val chatRoomId = aiChatRoomService.createAiChatRoom(userId) + val chatRoomId = aiFacade.produceAiChatRoom(userId) return ResponseHelper.success(chatRoomId) } + + @PostMapping("/ai/chat/room/prompt") + fun promptAiChatRoom( + @CurrentUser userId: UserId, + @RequestBody request: ChatRequest.Common, + ): SuccessResponseEntity { + val prompt = aiFacade.processAiMessage(userId, request.toChatRoomId(), request.toMessage()) + return ResponseHelper.success(AiChatMessageResponse.of(prompt)) + } } diff --git a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt new file mode 100644 index 000000000..4f17bf5a1 --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt @@ -0,0 +1,27 @@ +package org.chewing.v1.dto.response.chat + +import org.chewing.v1.model.chat.message.ChatAiMessage + +data class AiChatMessageResponse( + val messageId: String, + val type: String, + val senderId: String, + val timestamp: String, + val seqNumber: Int, + val text: String, + val senderType: String +) { + companion object { + fun of(promptMessage: ChatAiMessage): AiChatMessageResponse { + return AiChatMessageResponse( + messageId = promptMessage.messageId, + type = promptMessage.type.name.lowercase(), + senderId = promptMessage.senderId.id, + timestamp = promptMessage.timestamp.toString(), + seqNumber = promptMessage.roomSequence.sequence, + text = promptMessage.text, + senderType = promptMessage.senderType.name.lowercase() + ) + } + } +} diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 064f9a533..873d3064f 100755 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -13,6 +13,7 @@ spring: - ncp.yml - media.yml - openai.yml + - ai.yml - optional:file:../.env[.properties] - optional:file:./.env[.properties] mvc: 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 8a540d11e..c5726f4a7 100644 --- a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -1,6 +1,8 @@ package org.chewing.v1.facade +import org.chewing.v1.implementation.ai.AiUserGenerator 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.ChatRoomType import org.chewing.v1.model.user.UserId @@ -14,19 +16,21 @@ class AiFacade( private val aiChatRoomService: AiChatRoomService, private val aiPromptService: AiPromptService, private val chatLogService: ChatLogService, + private val aiUserGenerator: AiUserGenerator ) { fun processAiMessage( userId: UserId, chatRoomId: ChatRoomId, text: String, - ): String { + ): ChatAiMessage { val aiChatRoom = aiChatRoomService.getAiChatRoom(chatRoomId, userId) val chatSequence = aiChatRoomService.increaseDirectChatRoomSequence(aiChatRoom.chatRoomId) chatLogService.aiMessage(aiChatRoom.chatRoomId, userId, chatSequence, text, ChatRoomType.AI, SenderType.USER) val chatLogs = chatLogService.getChatLogs(aiChatRoom.chatRoomId, chatSequence.sequence, 0) - val aiMessage = aiPromptService.prompt(chatLogs) - chatLogService.aiMessage(aiChatRoom.chatRoomId, userId, chatSequence, aiMessage, ChatRoomType.AI, SenderType.AI) - return aiMessage + val aiPromptMessage = aiPromptService.prompt(chatLogs) + val aiUserId = aiUserGenerator.getAiUserId() + val aiChatMessage = chatLogService.aiMessage(aiChatRoom.chatRoomId, aiUserId, chatSequence, aiPromptMessage, ChatRoomType.AI, SenderType.AI) + return aiChatMessage } fun produceAiChatRoom( diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiUserGenerator.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiUserGenerator.kt new file mode 100644 index 000000000..083b223d0 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/ai/AiUserGenerator.kt @@ -0,0 +1,15 @@ +package org.chewing.v1.implementation.ai + +import org.chewing.v1.model.user.UserId +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component + +@Component +class AiUserGenerator( + @Value("\${ai.user-id}") + private val aiUserId: String, +) { + fun getAiUserId(): UserId { + return UserId.of(aiUserId) + } +} 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 0a2728691..edf3d9fd2 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 @@ -162,9 +162,10 @@ class ChatLogService( text: String, chatRoomType: ChatRoomType, senderType: SenderType, - ) { + ): ChatAiMessage { val chatMessage = chatGenerator.generateAiMessage(chatRoomId, userId, roomSequence, text, chatRoomType, senderType) chatAppender.appendChatLog(chatMessage) + return chatMessage } fun getsLatestChatLog(chatRoomIds: List): List { diff --git a/domain/src/main/resources/ai.yml b/domain/src/main/resources/ai.yml new file mode 100644 index 000000000..0a89d0158 --- /dev/null +++ b/domain/src/main/resources/ai.yml @@ -0,0 +1,12 @@ +ai: + user-id: ai_assistant + +--- + +ai: + user-id: ai_assistant + +--- + +ai: + user-id: ai_assistant From c71d208865b1a6b080a4529525961667d5b44f46 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Mon, 28 Apr 2025 22:50:24 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20aiChatRoomRepository=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chewing/v1/controller/ai/AiController.kt | 2 +- .../response/chat/AiChatMessageResponse.kt | 4 +- .../kotlin/org/chewing/v1/facade/AiFacade.kt | 4 +- .../v1/jpaentity/chat/AiChatRoomJpaEntity.kt | 46 +++++++++++++++++++ .../chat/AiChatRoomJpaRepository.kt | 9 ++++ .../jpa/chat/AiChatRoomRepositoryImpl.kt | 11 +++-- 6 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 storage/src/main/kotlin/org/chewing/v1/jpaentity/chat/AiChatRoomJpaEntity.kt create mode 100644 storage/src/main/kotlin/org/chewing/v1/jparepository/chat/AiChatRoomJpaRepository.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 7a666d7cd..aeb96ec05 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 @@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody @Controller class AiController( private val aiFacade: AiFacade, - private val aiChatRoomService: AiChatRoomService + private val aiChatRoomService: AiChatRoomService, ) { @PostMapping("/ai/chat/room") fun createAiChatRoom( diff --git a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt index 4f17bf5a1..5d5b357be 100644 --- a/api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt +++ b/api/src/main/kotlin/org/chewing/v1/dto/response/chat/AiChatMessageResponse.kt @@ -9,7 +9,7 @@ data class AiChatMessageResponse( val timestamp: String, val seqNumber: Int, val text: String, - val senderType: String + val senderType: String, ) { companion object { fun of(promptMessage: ChatAiMessage): AiChatMessageResponse { @@ -20,7 +20,7 @@ data class AiChatMessageResponse( timestamp = promptMessage.timestamp.toString(), seqNumber = promptMessage.roomSequence.sequence, text = promptMessage.text, - senderType = promptMessage.senderType.name.lowercase() + senderType = promptMessage.senderType.name.lowercase(), ) } } 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 c5726f4a7..bfc3af18e 100644 --- a/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt +++ b/domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt @@ -16,7 +16,7 @@ class AiFacade( private val aiChatRoomService: AiChatRoomService, private val aiPromptService: AiPromptService, private val chatLogService: ChatLogService, - private val aiUserGenerator: AiUserGenerator + private val aiUserGenerator: AiUserGenerator, ) { fun processAiMessage( userId: UserId, @@ -36,6 +36,6 @@ class AiFacade( fun produceAiChatRoom( userId: UserId, ): ChatRoomId { - return aiChatRoomService.createAiChatRoom(userId) + return aiChatRoomService.createAiChatRoom(userId) } } diff --git a/storage/src/main/kotlin/org/chewing/v1/jpaentity/chat/AiChatRoomJpaEntity.kt b/storage/src/main/kotlin/org/chewing/v1/jpaentity/chat/AiChatRoomJpaEntity.kt new file mode 100644 index 000000000..4b76de24c --- /dev/null +++ b/storage/src/main/kotlin/org/chewing/v1/jpaentity/chat/AiChatRoomJpaEntity.kt @@ -0,0 +1,46 @@ +package org.chewing.v1.jpaentity.chat + +import jakarta.persistence.Entity +import jakarta.persistence.Enumerated +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.chewing.v1.model.chat.room.AiChatRoomInfo +import org.chewing.v1.model.chat.room.ChatRoomId +import org.chewing.v1.model.chat.room.ChatRoomMemberStatus +import org.chewing.v1.model.user.UserId +import org.hibernate.annotations.DynamicInsert +import java.util.UUID + +@DynamicInsert +@Entity +@Table(name = "ai_chat_room", schema = "chewing") +class AiChatRoomJpaEntity( + @Id + private val chatRoomId: String = UUID.randomUUID().toString(), + + private val userId: String, + + @Enumerated + var status: ChatRoomMemberStatus, +) { + companion object { + fun generate(userId: UserId): AiChatRoomJpaEntity { + return AiChatRoomJpaEntity( + userId = userId.id, + status = ChatRoomMemberStatus.NORMAL, + ) + } + } + + fun toChatRoomId(): ChatRoomId { + return ChatRoomId.of(chatRoomId) + } + + fun toAiChatRoomInfo(): AiChatRoomInfo { + return AiChatRoomInfo.of( + chatRoomId = ChatRoomId.of(chatRoomId), + userId = UserId.of(userId), + status = status, + ) + } +} diff --git a/storage/src/main/kotlin/org/chewing/v1/jparepository/chat/AiChatRoomJpaRepository.kt b/storage/src/main/kotlin/org/chewing/v1/jparepository/chat/AiChatRoomJpaRepository.kt new file mode 100644 index 000000000..22b1c9c2b --- /dev/null +++ b/storage/src/main/kotlin/org/chewing/v1/jparepository/chat/AiChatRoomJpaRepository.kt @@ -0,0 +1,9 @@ +package org.chewing.v1.jparepository.chat + +import org.chewing.v1.jpaentity.chat.AiChatRoomJpaEntity +import org.springframework.data.jpa.repository.JpaRepository +import java.util.Optional + +internal interface AiChatRoomJpaRepository : JpaRepository { + fun findByChatRoomIdAndUserId(chatRoomId: String, userId: String): Optional +} diff --git a/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt index 3a4fbb100..cb014853a 100644 --- a/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt +++ b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/chat/AiChatRoomRepositoryImpl.kt @@ -1,5 +1,7 @@ package org.chewing.v1.repository.jpa.chat +import org.chewing.v1.jpaentity.chat.AiChatRoomJpaEntity +import org.chewing.v1.jparepository.chat.AiChatRoomJpaRepository import org.chewing.v1.model.chat.room.AiChatRoomInfo import org.chewing.v1.model.chat.room.ChatRoomId import org.chewing.v1.model.user.UserId @@ -7,15 +9,18 @@ import org.chewing.v1.repository.chat.AiChatRoomRepository import org.springframework.stereotype.Repository @Repository -internal class AiChatRoomRepositoryImpl() : AiChatRoomRepository { +internal class AiChatRoomRepositoryImpl( + private val aiChatRoomJpaRepository: AiChatRoomJpaRepository, +) : AiChatRoomRepository { override fun append(userId: UserId): ChatRoomId { - TODO("Not yet implemented") + return aiChatRoomJpaRepository.save(AiChatRoomJpaEntity.generate(userId)).toChatRoomId() } override fun readInfo( chatRoomId: ChatRoomId, userId: UserId, ): AiChatRoomInfo? { - TODO("Not yet implemented") + return aiChatRoomJpaRepository.findByChatRoomIdAndUserId(chatRoomId.id, userId.id).map { it.toAiChatRoomInfo() } + .orElse(null) } } From 354cdcab5739590c7b24ce970f6f1fc549d3d948 Mon Sep 17 00:00:00 2001 From: banseok1216 Date: Tue, 29 Apr 2025 13:54:33 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=ED=94=BC=EB=93=9C=20=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=20=EA=B8=B0=EB=8A=A5=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/docs/asciidoc/Report-API.adoc | 19 +++++ .../v1/controller/report/ReportController.kt | 28 +++++++ .../v1/dto/request/report/ReportRequest.kt | 17 ++++ .../v1/controller/ReportControllerTest.kt | 81 +++++++++++++++++++ .../implementation/report/ReportAppender.kt | 16 ++++ .../org/chewing/v1/model/report/Report.kt | 12 +++ .../v1/model/report/ReportTargetType.kt | 5 ++ .../v1/repository/report/ReportRepository.kt | 13 +++ .../v1/service/report/ReportService.kt | 16 ++++ .../v1/jpaentity/report/ReportJpaEntity.kt | 36 +++++++++ .../report/ReportJpaRepository.kt | 6 ++ .../jpa/report/ReportRepositoryImpl.kt | 27 +++++++ 12 files changed, 276 insertions(+) create mode 100644 api/src/docs/asciidoc/Report-API.adoc create mode 100644 api/src/main/kotlin/org/chewing/v1/controller/report/ReportController.kt create mode 100644 api/src/main/kotlin/org/chewing/v1/dto/request/report/ReportRequest.kt create mode 100644 api/src/test/kotlin/org/chewing/v1/controller/ReportControllerTest.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/implementation/report/ReportAppender.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/report/Report.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/model/report/ReportTargetType.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/repository/report/ReportRepository.kt create mode 100644 domain/src/main/kotlin/org/chewing/v1/service/report/ReportService.kt create mode 100644 storage/src/main/kotlin/org/chewing/v1/jpaentity/report/ReportJpaEntity.kt create mode 100644 storage/src/main/kotlin/org/chewing/v1/jparepository/report/ReportJpaRepository.kt create mode 100644 storage/src/main/kotlin/org/chewing/v1/repository/jpa/report/ReportRepositoryImpl.kt diff --git a/api/src/docs/asciidoc/Report-API.adoc b/api/src/docs/asciidoc/Report-API.adoc new file mode 100644 index 000000000..b2d024a41 --- /dev/null +++ b/api/src/docs/asciidoc/Report-API.adoc @@ -0,0 +1,19 @@ +[[Report-API]] += Report API + +== 1. 피드 신고하기 04/19 수정 + +=== 요청(Request) +include::{snippets}/report-controller-test/report-feed/http-request.adoc[] + +=== 요청 헤더(Request Headers) +include::{snippets}/report-controller-test/report-feed/request-headers.adoc[] + +=== 요청 필드 설명(Request Fields) +include::{snippets}/report-controller-test/report-feed/request-fields.adoc[] + +=== 응답(Response) +include::{snippets}/report-controller-test/report-feed/http-response.adoc[] + +=== 응답 필드 설명(Response Fields) +include::{snippets}/report-controller-test/report-feed/response-fields.adoc[] diff --git a/api/src/main/kotlin/org/chewing/v1/controller/report/ReportController.kt b/api/src/main/kotlin/org/chewing/v1/controller/report/ReportController.kt new file mode 100644 index 000000000..f144ecfd4 --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/controller/report/ReportController.kt @@ -0,0 +1,28 @@ +package org.chewing.v1.controller.report + +import org.chewing.v1.dto.request.report.ReportRequest +import org.chewing.v1.model.user.UserId +import org.chewing.v1.response.SuccessOnlyResponse +import org.chewing.v1.service.report.ReportService +import org.chewing.v1.util.aliases.SuccessResponseEntity +import org.chewing.v1.util.helper.ResponseHelper +import org.chewing.v1.util.security.CurrentUser +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/report") +class ReportController( + private val reportService: ReportService, +) { + @PostMapping("/feed") + fun reportFeed( + @RequestBody request: ReportRequest.Feed, + @CurrentUser userId: UserId, + ): SuccessResponseEntity { + reportService.reportFeed(userId, request.toFeedId(), request.toReport()) + return ResponseHelper.successOnly() + } +} diff --git a/api/src/main/kotlin/org/chewing/v1/dto/request/report/ReportRequest.kt b/api/src/main/kotlin/org/chewing/v1/dto/request/report/ReportRequest.kt new file mode 100644 index 000000000..42e9646eb --- /dev/null +++ b/api/src/main/kotlin/org/chewing/v1/dto/request/report/ReportRequest.kt @@ -0,0 +1,17 @@ +package org.chewing.v1.dto.request.report + +import org.chewing.v1.model.feed.FeedId +import org.chewing.v1.model.report.Report +import org.chewing.v1.model.report.ReportTargetType + +class ReportRequest { + data class Feed( + val feedId: String, + val reason: String, + ) { + fun toReport(): Report { + return Report.of(ReportTargetType.FEED, reason) + } + fun toFeedId() = FeedId.of(feedId) + } +} diff --git a/api/src/test/kotlin/org/chewing/v1/controller/ReportControllerTest.kt b/api/src/test/kotlin/org/chewing/v1/controller/ReportControllerTest.kt new file mode 100644 index 000000000..d2126e680 --- /dev/null +++ b/api/src/test/kotlin/org/chewing/v1/controller/ReportControllerTest.kt @@ -0,0 +1,81 @@ +package org.chewing.v1.controller + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +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.RestDocsUtils.responseSuccessFields +import org.chewing.v1.controller.report.ReportController +import org.chewing.v1.dto.request.report.ReportRequest +import org.chewing.v1.model.user.UserId +import org.chewing.v1.service.report.ReportService +import org.chewing.v1.util.converter.StringToFeedTypeConverter +import org.chewing.v1.util.handler.GlobalExceptionHandler +import org.chewing.v1.util.security.UserArgumentResolver +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import org.springframework.restdocs.payload.PayloadDocumentation.requestFields +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.test.context.ActiveProfiles + +@ActiveProfiles("test") +class ReportControllerTest : RestDocsTest() { + + private lateinit var reportService: ReportService + private lateinit var reportController: ReportController + private lateinit var exceptionHandler: GlobalExceptionHandler + private lateinit var userArgumentResolver: UserArgumentResolver + private lateinit var feedTypeConverter: StringToFeedTypeConverter + + @BeforeEach + fun setUp() { + reportService = mockk() + exceptionHandler = GlobalExceptionHandler() + userArgumentResolver = UserArgumentResolver() + feedTypeConverter = StringToFeedTypeConverter() + reportController = ReportController(reportService) + mockMvc = mockController(reportController, exceptionHandler, userArgumentResolver) + val userId = UserId.of("testUserId") + val authentication = UsernamePasswordAuthenticationToken(userId, null) + SecurityContextHolder.getContext().authentication = authentication + } + + @Test + @DisplayName("피드 신고 API 테스트") + fun reportFeed() { + val requestBody = ReportRequest.Feed( + feedId = "testFeedId", + reason = "신고 사유", + ) + + every { reportService.reportFeed(any(), any(), any()) } just Runs + + given() + .setupAuthenticatedJsonRequest() + .body(requestBody) + .post("/api/report/feed") + .then() + .assertCommonSuccessResponse() + .apply( + document( + "{class-name}/{method-name}", + requestPreprocessor(), + responsePreprocessor(), + requestFields( + fieldWithPath("feedId").description("신고할 피드 ID"), + fieldWithPath("reason").description("신고 사유"), + ), + requestAccessTokenFields(), + responseSuccessFields(), + ), + ) + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/implementation/report/ReportAppender.kt b/domain/src/main/kotlin/org/chewing/v1/implementation/report/ReportAppender.kt new file mode 100644 index 000000000..94eac512b --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/implementation/report/ReportAppender.kt @@ -0,0 +1,16 @@ +package org.chewing.v1.implementation.report + +import org.chewing.v1.model.feed.FeedId +import org.chewing.v1.model.report.Report +import org.chewing.v1.model.user.UserId +import org.chewing.v1.repository.report.ReportRepository +import org.springframework.stereotype.Component + +@Component +class ReportAppender( + private val reportRepository: ReportRepository, +) { + fun appendReport(userId: UserId, feedId: FeedId, report: Report) { + reportRepository.reportFeed(userId, feedId, report) + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/report/Report.kt b/domain/src/main/kotlin/org/chewing/v1/model/report/Report.kt new file mode 100644 index 000000000..ba09263ab --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/report/Report.kt @@ -0,0 +1,12 @@ +package org.chewing.v1.model.report + +class Report( + val targetType: ReportTargetType, + val reason: String, +) { + companion object { + fun of(targetType: ReportTargetType, reason: String): Report { + return Report(targetType, reason) + } + } +} diff --git a/domain/src/main/kotlin/org/chewing/v1/model/report/ReportTargetType.kt b/domain/src/main/kotlin/org/chewing/v1/model/report/ReportTargetType.kt new file mode 100644 index 000000000..217aa8013 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/model/report/ReportTargetType.kt @@ -0,0 +1,5 @@ +package org.chewing.v1.model.report + +enum class ReportTargetType { + FEED, +} diff --git a/domain/src/main/kotlin/org/chewing/v1/repository/report/ReportRepository.kt b/domain/src/main/kotlin/org/chewing/v1/repository/report/ReportRepository.kt new file mode 100644 index 000000000..6ced58b26 --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/repository/report/ReportRepository.kt @@ -0,0 +1,13 @@ +package org.chewing.v1.repository.report + +import org.chewing.v1.model.feed.FeedId +import org.chewing.v1.model.report.Report +import org.chewing.v1.model.user.UserId + +interface ReportRepository { + fun reportFeed( + userId: UserId, + feedId: FeedId, + report: Report, + ) +} diff --git a/domain/src/main/kotlin/org/chewing/v1/service/report/ReportService.kt b/domain/src/main/kotlin/org/chewing/v1/service/report/ReportService.kt new file mode 100644 index 000000000..ce7ad821b --- /dev/null +++ b/domain/src/main/kotlin/org/chewing/v1/service/report/ReportService.kt @@ -0,0 +1,16 @@ +package org.chewing.v1.service.report + +import org.chewing.v1.implementation.report.ReportAppender +import org.chewing.v1.model.feed.FeedId +import org.chewing.v1.model.report.Report +import org.chewing.v1.model.user.UserId +import org.springframework.stereotype.Service + +@Service +class ReportService( + private val reportAppender: ReportAppender, +) { + fun reportFeed(userId: UserId, feedId: FeedId, report: Report) { + reportAppender.appendReport(userId, feedId, report) + } +} diff --git a/storage/src/main/kotlin/org/chewing/v1/jpaentity/report/ReportJpaEntity.kt b/storage/src/main/kotlin/org/chewing/v1/jpaentity/report/ReportJpaEntity.kt new file mode 100644 index 000000000..7db8cbcf2 --- /dev/null +++ b/storage/src/main/kotlin/org/chewing/v1/jpaentity/report/ReportJpaEntity.kt @@ -0,0 +1,36 @@ +package org.chewing.v1.jpaentity.report + +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.chewing.v1.model.report.Report +import org.chewing.v1.model.report.ReportTargetType +import org.chewing.v1.model.user.UserId +import org.hibernate.annotations.DynamicInsert +import java.util.UUID + +@DynamicInsert +@Entity +@Table(name = "report", schema = "chewing") +class ReportJpaEntity( + @Id + private val reportId: String = UUID.randomUUID().toString(), + @Enumerated(EnumType.STRING) + private val targetType: ReportTargetType, + private val targetId: String, + private val userId: String, + private val reason: String, +) { + companion object { + fun generate(userId: UserId, targetId: String, report: Report): ReportJpaEntity { + return ReportJpaEntity( + targetType = report.targetType, + targetId = targetId, + userId = userId.id, + reason = report.reason, + ) + } + } +} diff --git a/storage/src/main/kotlin/org/chewing/v1/jparepository/report/ReportJpaRepository.kt b/storage/src/main/kotlin/org/chewing/v1/jparepository/report/ReportJpaRepository.kt new file mode 100644 index 000000000..cc0224dc2 --- /dev/null +++ b/storage/src/main/kotlin/org/chewing/v1/jparepository/report/ReportJpaRepository.kt @@ -0,0 +1,6 @@ +package org.chewing.v1.jparepository.report + +import org.chewing.v1.jpaentity.report.ReportJpaEntity +import org.springframework.data.jpa.repository.JpaRepository + +internal interface ReportJpaRepository : JpaRepository diff --git a/storage/src/main/kotlin/org/chewing/v1/repository/jpa/report/ReportRepositoryImpl.kt b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/report/ReportRepositoryImpl.kt new file mode 100644 index 000000000..cca180d1e --- /dev/null +++ b/storage/src/main/kotlin/org/chewing/v1/repository/jpa/report/ReportRepositoryImpl.kt @@ -0,0 +1,27 @@ +package org.chewing.v1.repository.jpa.report + +import org.chewing.v1.jpaentity.report.ReportJpaEntity +import org.chewing.v1.jparepository.report.ReportJpaRepository +import org.chewing.v1.model.feed.FeedId +import org.chewing.v1.model.report.Report +import org.chewing.v1.model.user.UserId +import org.chewing.v1.repository.report.ReportRepository +import org.springframework.stereotype.Repository + +@Repository +internal class ReportRepositoryImpl( + private val reportJpaRepository: ReportJpaRepository, +) : ReportRepository { + override fun reportFeed( + userId: UserId, + feedId: FeedId, + report: Report, + ) { + val entity = ReportJpaEntity.generate( + userId, + feedId.id, + report, + ) + reportJpaRepository.save(entity) + } +}