diff --git a/build.gradle b/build.gradle index ee706d7e..393f7f51 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ configurations { repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } } sourceSets { @@ -37,6 +38,8 @@ sourceSets { } dependencies { + implementation platform('org.springframework.ai:spring-ai-bom:0.8.1') + implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/src/main/java/com/space/server/common/config/OpenAiConfig.java b/src/main/java/com/space/server/common/config/OpenAiConfig.java new file mode 100644 index 00000000..84b827c6 --- /dev/null +++ b/src/main/java/com/space/server/common/config/OpenAiConfig.java @@ -0,0 +1,23 @@ +package com.space.server.common.config; + +import org.springframework.ai.openai.OpenAiChatClient; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenAiConfig { + @Value("${spring.ai.openai.api-key}") + private String openAiApiKey; + + @Bean + public OpenAiApi openAiApi() { + return new OpenAiApi(openAiApiKey); + } + + @Bean + public OpenAiChatClient openAiChatClient(OpenAiApi openAiApi) { + return new OpenAiChatClient(openAiApi); + } +} diff --git a/src/main/java/com/space/server/domain/chat/presentation/ChatController.java b/src/main/java/com/space/server/domain/chat/presentation/ChatController.java index 53523ee3..4d4b18bd 100644 --- a/src/main/java/com/space/server/domain/chat/presentation/ChatController.java +++ b/src/main/java/com/space/server/domain/chat/presentation/ChatController.java @@ -4,10 +4,7 @@ import com.space.server.domain.ai.service.implementation.ChatTuner; import com.space.server.domain.chat.presentation.dto.request.CreateChatRequest; import com.space.server.domain.chat.presentation.dto.request.ReadQuizAndUserRequest; -import com.space.server.domain.chat.presentation.dto.response.ChatResponse; -import com.space.server.domain.chat.presentation.dto.response.CountChatResponse; -import com.space.server.domain.chat.presentation.dto.response.ReadKeyWordsResponse; -import com.space.server.domain.chat.presentation.dto.response.ReadSuccessChatResponse; +import com.space.server.domain.chat.presentation.dto.response.*; import com.space.server.domain.chat.service.CommandChatService; import com.space.server.domain.chat.service.QueryChatService; import com.space.server.domain.state.service.QueryStateService; @@ -60,6 +57,11 @@ public ReadKeyWordsResponse readMostKeyWords() { ); } + @GetMapping("/chats/typo") + public VocabularyFeedbackResponse readTypo() { + return queryChatService.findTypo(getMemberId()); + } + @PostMapping("/chats/count") public CountChatResponse countChatByQuiz(@RequestBody ReadQuizAndUserRequest request) { return CountChatResponse.of( diff --git a/src/main/java/com/space/server/domain/chat/presentation/dto/response/VocabularyFeedbackResponse.java b/src/main/java/com/space/server/domain/chat/presentation/dto/response/VocabularyFeedbackResponse.java new file mode 100644 index 00000000..3cdac956 --- /dev/null +++ b/src/main/java/com/space/server/domain/chat/presentation/dto/response/VocabularyFeedbackResponse.java @@ -0,0 +1,7 @@ +package com.space.server.domain.chat.presentation.dto.response; + +public record VocabularyFeedbackResponse( + String vocabularyAnalysis, + String aiGeneratedFeedback +) { +} diff --git a/src/main/java/com/space/server/domain/chat/service/QueryChatService.java b/src/main/java/com/space/server/domain/chat/service/QueryChatService.java index 12ec379d..8d95cd03 100644 --- a/src/main/java/com/space/server/domain/chat/service/QueryChatService.java +++ b/src/main/java/com/space/server/domain/chat/service/QueryChatService.java @@ -2,6 +2,7 @@ import com.space.server.domain.chat.domain.Chat; import com.space.server.domain.chat.presentation.dto.response.ChatResponse; +import com.space.server.domain.chat.presentation.dto.response.VocabularyFeedbackResponse; import com.space.server.domain.chat.service.implementation.ChatAnalyzer; import com.space.server.domain.chat.service.implementation.ChatReader; import com.space.server.domain.quiz.service.implementation.QuizReader; @@ -10,6 +11,8 @@ import com.space.server.domain.state.service.implementation.StateReader; import com.space.server.domain.user.service.implementation.UserReader; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.openai.OpenAiChatClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,11 +20,13 @@ import java.util.function.Function; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class QueryChatService { + private final OpenAiChatClient chatClient; private final ChatReader chatReader; private final StateReader stateReader; private final QuizReader quizReader; @@ -59,6 +64,50 @@ public Map readMostKeyWords(Long userId) { )); } + public VocabularyFeedbackResponse findTypo(Long userId) { + // 사용자의 상태와 채팅 내용 조회 + List states = stateReader.findByUserId(userReader.findById(userId)); + List chats = chatReader.findAllChatByStates(states); + + // 모든 채팅 내용 병합 + String mergedChats = chats.stream() + .map(Chat::getUserChat) + .filter(Objects::nonNull) + .collect(Collectors.joining(" ")); + + // 어휘 오류 분석 프롬프트 + String analysisPrompt = String.format( + "다음 텍스트에서 가장 많이 틀린 어휘 상위 5개를 찾아주세요. " + + "각 어휘에 대해 몇 번 잘못 썼는지, 올바른 표현은 무엇인지 알려주세요. " + + "초등학생의 글입니다. 텍스트: %s", mergedChats + ); + + // 어휘 오류 분석 + String analysisResponse = chatClient.call(analysisPrompt); + + // 피드백 생성 프롬프트 + String feedbackPrompt = String.format( + "다음은 초등학생의 어휘 사용 분석입니다: %s\n\n" + + "이 분석을 바탕으로 부모님과 선생님을 위한 상세하고 구체적인 어휘력 향상 피드백을 만들어주세요. " + + "다음 사항을 포함해주세요:\n" + + "1. 발견된 어휘 오류의 구체적인 교정 방법\n" + + "2. 학생의 어휘력을 높이기 위한 맞춤형 학습 전략\n" + + "3. 연령에 적합한 어휘 학습 접근 방식\n" + + "4. 부모님과 선생님이 함께할 수 있는 실천 가능한 활동\n" + + "5. 장기적인 어휘력 향상을 위한 지속 가능한 방법", + analysisResponse + ); + + // AI 생성 피드백 요청 + String aiGeneratedFeedback = chatClient.call(feedbackPrompt); + + // 결과 객체 생성 + VocabularyFeedbackResponse feedback = new VocabularyFeedbackResponse(analysisResponse, aiGeneratedFeedback); + + return feedback; + } + + public Integer countChats(Long quizId, Long userId) { State state = stateReader.findByQuizIdAndUserId(quizReader.findById(quizId), userReader.findById(userId)) diff --git a/src/main/resources/application-common.yml b/src/main/resources/application-common.yml index 375feeac..4e5ff6ae 100644 --- a/src/main/resources/application-common.yml +++ b/src/main/resources/application-common.yml @@ -5,6 +5,13 @@ spring: username: ${DB_USER} password: ${DB_PASSWORD} + ai: + openai: + api-key: ${GPT_API_KEY} + chat: + options: + model: gpt-4o + logging: level: org: