diff --git a/app/build.gradle b/app/build.gradle index 84fe7760..870be73a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,8 @@ dependencies { implementation project(':auth:auth-impl') implementation project(':aws:aws-impl') implementation project(':quiz:quiz-impl') + implementation project(':util:util-impl') + implementation project(':global') implementation "org.springframework.boot:spring-boot-starter-actuator" annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' diff --git a/modules/aws/api/src/main/java/com/icc/qasker/aws/S3Service.java b/modules/aws/api/src/main/java/com/icc/qasker/aws/S3Service.java deleted file mode 100644 index 20339356..00000000 --- a/modules/aws/api/src/main/java/com/icc/qasker/aws/S3Service.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.icc.qasker.aws; - -import com.icc.qasker.aws.dto.FileExistStatusResponse; -import com.icc.qasker.aws.dto.PresignRequest; -import com.icc.qasker.aws.dto.PresignResponse; - -public interface S3Service { - - PresignResponse requestPresign(PresignRequest presignRequest); - - FileExistStatusResponse checkFileExistence(String originalFileName); -} - diff --git a/modules/aws/api/src/main/java/com/icc/qasker/util/S3Service.java b/modules/aws/api/src/main/java/com/icc/qasker/util/S3Service.java new file mode 100644 index 00000000..6014f0b5 --- /dev/null +++ b/modules/aws/api/src/main/java/com/icc/qasker/util/S3Service.java @@ -0,0 +1,13 @@ +package com.icc.qasker.util; + +import com.icc.qasker.util.dto.FileExistStatusResponse; +import com.icc.qasker.util.dto.PresignRequest; +import com.icc.qasker.util.dto.PresignResponse; + +public interface S3Service { + + PresignResponse requestPresign(PresignRequest presignRequest); + + FileExistStatusResponse checkFileExistence(String originalFileName); +} + diff --git a/modules/aws/api/src/main/java/com/icc/qasker/aws/S3ValidateService.java b/modules/aws/api/src/main/java/com/icc/qasker/util/S3ValidateService.java similarity index 86% rename from modules/aws/api/src/main/java/com/icc/qasker/aws/S3ValidateService.java rename to modules/aws/api/src/main/java/com/icc/qasker/util/S3ValidateService.java index 49542650..a97e93d5 100644 --- a/modules/aws/api/src/main/java/com/icc/qasker/aws/S3ValidateService.java +++ b/modules/aws/api/src/main/java/com/icc/qasker/util/S3ValidateService.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws; +package com.icc.qasker.util; public interface S3ValidateService { diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/controller/doc/S3ApiDoc.java b/modules/aws/api/src/main/java/com/icc/qasker/util/doc/S3ApiDoc.java similarity index 83% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/controller/doc/S3ApiDoc.java rename to modules/aws/api/src/main/java/com/icc/qasker/util/doc/S3ApiDoc.java index d33fc08a..b2267443 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/controller/doc/S3ApiDoc.java +++ b/modules/aws/api/src/main/java/com/icc/qasker/util/doc/S3ApiDoc.java @@ -1,8 +1,8 @@ -package com.icc.qasker.aws.controller.doc; +package com.icc.qasker.util.doc; -import com.icc.qasker.aws.dto.FileExistStatusResponse; -import com.icc.qasker.aws.dto.PresignRequest; -import com.icc.qasker.aws.dto.PresignResponse; +import com.icc.qasker.util.dto.FileExistStatusResponse; +import com.icc.qasker.util.dto.PresignRequest; +import com.icc.qasker.util.dto.PresignResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; diff --git a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/FileExistStatusResponse.java b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/FileExistStatusResponse.java similarity index 66% rename from modules/aws/api/src/main/java/com/icc/qasker/aws/dto/FileExistStatusResponse.java rename to modules/aws/api/src/main/java/com/icc/qasker/util/dto/FileExistStatusResponse.java index 404cf305..e8c89614 100644 --- a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/FileExistStatusResponse.java +++ b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/FileExistStatusResponse.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.dto; +package com.icc.qasker.util.dto; public record FileExistStatusResponse( Status status diff --git a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/PresignRequest.java b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/PresignRequest.java similarity index 93% rename from modules/aws/api/src/main/java/com/icc/qasker/aws/dto/PresignRequest.java rename to modules/aws/api/src/main/java/com/icc/qasker/util/dto/PresignRequest.java index 32700a64..25705816 100644 --- a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/PresignRequest.java +++ b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/PresignRequest.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.dto; +package com.icc.qasker.util.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/PresignResponse.java b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/PresignResponse.java similarity index 75% rename from modules/aws/api/src/main/java/com/icc/qasker/aws/dto/PresignResponse.java rename to modules/aws/api/src/main/java/com/icc/qasker/util/dto/PresignResponse.java index e6cb0ea9..e1d04e40 100644 --- a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/PresignResponse.java +++ b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/PresignResponse.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.dto; +package com.icc.qasker.util.dto; public record PresignResponse( String uploadUrl, diff --git a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/Status.java b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/Status.java similarity index 59% rename from modules/aws/api/src/main/java/com/icc/qasker/aws/dto/Status.java rename to modules/aws/api/src/main/java/com/icc/qasker/util/dto/Status.java index ea35012c..55588eb2 100644 --- a/modules/aws/api/src/main/java/com/icc/qasker/aws/dto/Status.java +++ b/modules/aws/api/src/main/java/com/icc/qasker/util/dto/Status.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.dto; +package com.icc.qasker.util.dto; public enum Status { NOT_EXIST, diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/config/MockS3ClientConfig.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/config/MockS3ClientConfig.java similarity index 97% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/config/MockS3ClientConfig.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/config/MockS3ClientConfig.java index 62b7fb3c..e0370434 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/config/MockS3ClientConfig.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/config/MockS3ClientConfig.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.config; +package com.icc.qasker.util.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/config/S3ClientConfig.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/config/S3ClientConfig.java similarity index 94% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/config/S3ClientConfig.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/config/S3ClientConfig.java index 616a4288..e2ed055e 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/config/S3ClientConfig.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/config/S3ClientConfig.java @@ -1,6 +1,6 @@ -package com.icc.qasker.aws.config; +package com.icc.qasker.util.config; -import com.icc.qasker.aws.properties.AwsS3Properties; +import com.icc.qasker.util.properties.AwsS3Properties; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/controller/S3Controller.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/controller/S3Controller.java similarity index 80% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/controller/S3Controller.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/controller/S3Controller.java index a1ff6259..c387906f 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/controller/S3Controller.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/controller/S3Controller.java @@ -1,10 +1,10 @@ -package com.icc.qasker.aws.controller; +package com.icc.qasker.util.controller; -import com.icc.qasker.aws.S3Service; -import com.icc.qasker.aws.controller.doc.S3ApiDoc; -import com.icc.qasker.aws.dto.FileExistStatusResponse; -import com.icc.qasker.aws.dto.PresignRequest; -import com.icc.qasker.aws.dto.PresignResponse; +import com.icc.qasker.util.S3Service; +import com.icc.qasker.util.doc.S3ApiDoc; +import com.icc.qasker.util.dto.FileExistStatusResponse; +import com.icc.qasker.util.dto.PresignRequest; +import com.icc.qasker.util.dto.PresignResponse; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/AwsCloudFrontProperties.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/properties/AwsCloudFrontProperties.java similarity index 82% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/AwsCloudFrontProperties.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/properties/AwsCloudFrontProperties.java index 7379db6b..afbb12b8 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/AwsCloudFrontProperties.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/properties/AwsCloudFrontProperties.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.properties; +package com.icc.qasker.util.properties; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/AwsS3Properties.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/properties/AwsS3Properties.java similarity index 89% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/AwsS3Properties.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/properties/AwsS3Properties.java index 9a144afc..54f4f7b8 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/AwsS3Properties.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/properties/AwsS3Properties.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.properties; +package com.icc.qasker.util.properties; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/LibreOfficeProperties.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/properties/LibreOfficeProperties.java similarity index 83% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/LibreOfficeProperties.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/properties/LibreOfficeProperties.java index c7fc159c..c14d52de 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/properties/LibreOfficeProperties.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/properties/LibreOfficeProperties.java @@ -1,4 +1,4 @@ -package com.icc.qasker.aws.properties; +package com.icc.qasker.util.properties; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/service/S3ServiceImpl.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/service/S3ServiceImpl.java similarity index 91% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/service/S3ServiceImpl.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/service/S3ServiceImpl.java index 6b6ba3f5..65c6a2bc 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/service/S3ServiceImpl.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/service/S3ServiceImpl.java @@ -1,13 +1,13 @@ -package com.icc.qasker.aws.service; - -import com.icc.qasker.aws.S3Service; -import com.icc.qasker.aws.S3ValidateService; -import com.icc.qasker.aws.dto.FileExistStatusResponse; -import com.icc.qasker.aws.dto.PresignRequest; -import com.icc.qasker.aws.dto.PresignResponse; -import com.icc.qasker.aws.dto.Status; -import com.icc.qasker.aws.properties.AwsCloudFrontProperties; -import com.icc.qasker.aws.properties.AwsS3Properties; +package com.icc.qasker.util.service; + +import com.icc.qasker.util.S3Service; +import com.icc.qasker.util.S3ValidateService; +import com.icc.qasker.util.dto.FileExistStatusResponse; +import com.icc.qasker.util.dto.PresignRequest; +import com.icc.qasker.util.dto.PresignResponse; +import com.icc.qasker.util.dto.Status; +import com.icc.qasker.util.properties.AwsCloudFrontProperties; +import com.icc.qasker.util.properties.AwsS3Properties; import com.icc.qasker.global.error.CustomException; import com.icc.qasker.global.error.ExceptionMessage; import java.net.URI; diff --git a/modules/aws/impl/src/main/java/com/icc/qasker/aws/service/S3ValidateServiceImpl.java b/modules/aws/impl/src/main/java/com/icc/qasker/util/service/S3ValidateServiceImpl.java similarity index 88% rename from modules/aws/impl/src/main/java/com/icc/qasker/aws/service/S3ValidateServiceImpl.java rename to modules/aws/impl/src/main/java/com/icc/qasker/util/service/S3ValidateServiceImpl.java index 578a2d6f..d7a4b85e 100644 --- a/modules/aws/impl/src/main/java/com/icc/qasker/aws/service/S3ValidateServiceImpl.java +++ b/modules/aws/impl/src/main/java/com/icc/qasker/util/service/S3ValidateServiceImpl.java @@ -1,8 +1,8 @@ -package com.icc.qasker.aws.service; +package com.icc.qasker.util.service; -import com.icc.qasker.aws.S3ValidateService; -import com.icc.qasker.aws.properties.AwsCloudFrontProperties; -import com.icc.qasker.aws.properties.AwsS3Properties; +import com.icc.qasker.util.S3ValidateService; +import com.icc.qasker.util.properties.AwsCloudFrontProperties; +import com.icc.qasker.util.properties.AwsS3Properties; import com.icc.qasker.global.error.CustomException; import com.icc.qasker.global.error.ExceptionMessage; import lombok.AllArgsConstructor; diff --git a/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java b/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java index c89e5f7e..8f51130f 100644 --- a/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java +++ b/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient.Builder; @@ -16,7 +17,8 @@ public class SlackNotifier { private final Builder restClientBuilder; private final SlackProperties slackProperties; - public void notifyText(String text) { + @Async + public void asyncNotifyText(String text) { boolean enabled = slackProperties.isEnabled(); String webhookUrl = slackProperties.getWebhookUrlNotify().toString(); if (!enabled || webhookUrl == null || webhookUrl.isBlank()) { diff --git a/modules/quiz/api/build.gradle b/modules/quiz/api/build.gradle index 620edb71..473d5f30 100644 --- a/modules/quiz/api/build.gradle +++ b/modules/quiz/api/build.gradle @@ -1,3 +1,7 @@ plugins { id 'java-library' +} + +dependencies { + implementation project(":global") } \ No newline at end of file diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ExplanationService.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ExplanationService.java index b9281143..0e2c20d4 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ExplanationService.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ExplanationService.java @@ -1,6 +1,6 @@ package com.icc.qasker.quiz; -import com.icc.qasker.quiz.dto.response.ExplanationResponse; +import com.icc.qasker.quiz.dto.feResponse.ExplanationResponse; public interface ExplanationService { diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/GenerationService.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/GenerationService.java index 078e4727..595b02eb 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/GenerationService.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/GenerationService.java @@ -1,11 +1,11 @@ package com.icc.qasker.quiz; -import com.icc.qasker.quiz.dto.request.FeGenerationRequest; -import com.icc.qasker.quiz.dto.response.GenerationResponse; +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; public interface GenerationService { - GenerationResponse processGenerationRequest(FeGenerationRequest feGenerationRequest, + SseEmitter processGenerationRequest(GenerationRequest generationRequest, String userId); } diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ProblemSetService.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ProblemSetService.java index d24c48f1..6453862b 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ProblemSetService.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/ProblemSetService.java @@ -1,6 +1,6 @@ package com.icc.qasker.quiz; -import com.icc.qasker.quiz.dto.response.ProblemSetResponse; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; public interface ProblemSetService { diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/SpecificExplanationService.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/SpecificExplanationService.java index 090bbe7e..422f6b07 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/SpecificExplanationService.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/SpecificExplanationService.java @@ -1,6 +1,6 @@ package com.icc.qasker.quiz; -import com.icc.qasker.quiz.dto.response.SpecificExplanationResponse; +import com.icc.qasker.quiz.dto.feResponse.SpecificExplanationResponse; public interface SpecificExplanationService { diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/ExplanationApiDoc.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/ExplanationApiDoc.java similarity index 83% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/ExplanationApiDoc.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/ExplanationApiDoc.java index f11d0086..83960aca 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/ExplanationApiDoc.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/ExplanationApiDoc.java @@ -1,6 +1,6 @@ -package com.icc.qasker.quiz.controller.doc; +package com.icc.qasker.quiz.doc; -import com.icc.qasker.quiz.dto.response.ExplanationResponse; +import com.icc.qasker.quiz.dto.feResponse.ExplanationResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/GenerationApiDoc.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/GenerationApiDoc.java new file mode 100644 index 00000000..c1f12a78 --- /dev/null +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/GenerationApiDoc.java @@ -0,0 +1,20 @@ +package com.icc.qasker.quiz.doc; + +import com.icc.qasker.global.annotation.UserId; +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Tag(name = "Generation", description = "생성 관련 API") +public interface GenerationApiDoc { + + @Operation(summary = "문제를 생성한다") + @PostMapping + SseEmitter postProblemSetId( + @UserId + String userId, + @RequestBody GenerationRequest generationRequest); +} diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/ProblemSetApiDoc.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/ProblemSetApiDoc.java similarity index 84% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/ProblemSetApiDoc.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/ProblemSetApiDoc.java index f0a2118a..a77cdf54 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/ProblemSetApiDoc.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/ProblemSetApiDoc.java @@ -1,6 +1,6 @@ -package com.icc.qasker.quiz.controller.doc; +package com.icc.qasker.quiz.doc; -import com.icc.qasker.quiz.dto.response.ProblemSetResponse; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/SpecificExplanationApiDoc.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/SpecificExplanationApiDoc.java similarity index 79% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/SpecificExplanationApiDoc.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/SpecificExplanationApiDoc.java index 6700fd34..100aed27 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/SpecificExplanationApiDoc.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/SpecificExplanationApiDoc.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.controller.doc; +package com.icc.qasker.quiz.doc; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/GenerationRequestToAI.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/GenerationRequestToAI.java new file mode 100644 index 00000000..45848dd7 --- /dev/null +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/GenerationRequestToAI.java @@ -0,0 +1,29 @@ +package com.icc.qasker.quiz.dto.aiRequest; + +import com.icc.qasker.quiz.dto.feRequest.enums.DifficultyType; +import com.icc.qasker.quiz.dto.feRequest.enums.QuizType; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record GenerationRequestToAI( + + @NotBlank(message = "url이 존재하지 않습니다.") + String uploadedUrl, + @Min(value = 5, message = "quizCount는 5이상입니다.") + @Max(value = 50, message = "quizCount는 50이하입니다.") + int quizCount, + @NotNull(message = "quizType이 null입니다.") + QuizType quizType, + @NotNull(message = "difficultyType가 null입니다.") + DifficultyType difficultyType, + @NotNull(message = "pageNumbers가 null입니다.") + @Size(min = 1, max = 150, message = "pageNumbers는 1개 이상 150 이하이어야 합니다.") + List pageNumbers + +) { + +}; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/SpecificExplanationRequest.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/SpecificExplanationRequestToAI.java similarity index 61% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/SpecificExplanationRequest.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/SpecificExplanationRequestToAI.java index 462fc0c4..55b07707 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/SpecificExplanationRequest.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/SpecificExplanationRequestToAI.java @@ -1,6 +1,6 @@ -package com.icc.qasker.quiz.dto.request; +package com.icc.qasker.quiz.dto.aiRequest; -import com.icc.qasker.quiz.dto.response.QuizGeneratedByAI.SelectionsOfAi; +import com.icc.qasker.quiz.dto.aiResponse.QuizGeneratedFromAI.SelectionsOfAi; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; @@ -11,7 +11,7 @@ @AllArgsConstructor @NoArgsConstructor @Getter -public class SpecificExplanationRequest { +public class SpecificExplanationRequestToAI { private String title; private List selections; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/AiGenerationResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/GenerationResponseFromAI.java similarity index 70% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/AiGenerationResponse.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/GenerationResponseFromAI.java index 48e7c089..8644f3b1 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/AiGenerationResponse.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/GenerationResponseFromAI.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.response; +package com.icc.qasker.quiz.dto.aiResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; @@ -10,9 +10,9 @@ @Getter @NoArgsConstructor @AllArgsConstructor -public class AiGenerationResponse { +public class GenerationResponseFromAI { @NotEmpty(message = "quiz가 null입니다.") @Valid - private List quiz; + private List quiz; } diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/QuizGeneratedByAI.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/QuizGeneratedFromAI.java similarity index 92% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/QuizGeneratedByAI.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/QuizGeneratedFromAI.java index e9bab09c..b6bef589 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/QuizGeneratedByAI.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiResponse/QuizGeneratedFromAI.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.response; +package com.icc.qasker.quiz.dto.aiResponse; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -11,7 +11,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor -public class QuizGeneratedByAI { +public class QuizGeneratedFromAI { @NotNull(message = "number가 null입니다.") private int number; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/FeGenerationRequest.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/GenerationRequest.java similarity index 81% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/FeGenerationRequest.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/GenerationRequest.java index 132bb3ab..4ff808d9 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/FeGenerationRequest.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/GenerationRequest.java @@ -1,7 +1,7 @@ -package com.icc.qasker.quiz.dto.request; +package com.icc.qasker.quiz.dto.feRequest; -import com.icc.qasker.quiz.dto.request.enums.DifficultyType; -import com.icc.qasker.quiz.dto.request.enums.QuizType; +import com.icc.qasker.quiz.dto.feRequest.enums.DifficultyType; +import com.icc.qasker.quiz.dto.feRequest.enums.QuizType; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; @@ -9,7 +9,7 @@ import jakarta.validation.constraints.Size; import java.util.List; -public record FeGenerationRequest( +public record GenerationRequest( @NotBlank(message = "url이 존재하지 않습니다.") String uploadedUrl, diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/enums/DifficultyType.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/enums/DifficultyType.java similarity index 58% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/enums/DifficultyType.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/enums/DifficultyType.java index b010897e..b5e366da 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/enums/DifficultyType.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/enums/DifficultyType.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.request.enums; +package com.icc.qasker.quiz.dto.feRequest.enums; public enum DifficultyType { RECALL, diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/enums/QuizType.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/enums/QuizType.java similarity index 54% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/enums/QuizType.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/enums/QuizType.java index f0cff402..45c8c01e 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/enums/QuizType.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feRequest/enums/QuizType.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.request.enums; +package com.icc.qasker.quiz.dto.feRequest.enums; public enum QuizType { MULTIPLE, diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ExplanationResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ExplanationResponse.java similarity index 81% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ExplanationResponse.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ExplanationResponse.java index e9159d1e..8baa452f 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ExplanationResponse.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ExplanationResponse.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.response; +package com.icc.qasker.quiz.dto.feResponse; import java.util.List; import lombok.AllArgsConstructor; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ProblemSetResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java similarity index 87% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ProblemSetResponse.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java index 40b16df8..c9eea7a7 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ProblemSetResponse.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.response; +package com.icc.qasker.quiz.dto.feResponse; import java.util.List; import lombok.AllArgsConstructor; @@ -10,6 +10,8 @@ @NoArgsConstructor public class ProblemSetResponse { + private String problemSetId; + private int totalCount; private List quiz; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ResultResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ResultResponse.java similarity index 84% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ResultResponse.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ResultResponse.java index 6aac1b4a..c632fec1 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/ResultResponse.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ResultResponse.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.response; +package com.icc.qasker.quiz.dto.feResponse; import java.util.List; import lombok.AllArgsConstructor; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/SpecificExplanationResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/SpecificExplanationResponse.java similarity index 85% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/SpecificExplanationResponse.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/SpecificExplanationResponse.java index 398aad62..8a9b0447 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/SpecificExplanationResponse.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/SpecificExplanationResponse.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.response; +package com.icc.qasker.quiz.dto.feResponse; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/GenerationResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/GenerationResponse.java deleted file mode 100644 index 00825f2e..00000000 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/GenerationResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.icc.qasker.quiz.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class GenerationResponse { - - private String problemSetId; -} diff --git a/modules/quiz/impl/build.gradle b/modules/quiz/impl/build.gradle index fad8e322..024c6741 100644 --- a/modules/quiz/impl/build.gradle +++ b/modules/quiz/impl/build.gradle @@ -1,12 +1,16 @@ plugins { - id 'java-library' + id "java-library" } dependencies { implementation project(":global") implementation project(":aws:aws-api") implementation project(":quiz:quiz-api") - implementation 'org.apache.httpcomponents.client5:httpclient5:5.3.1' + implementation "io.github.resilience4j:resilience4j-spring-boot3:2.3.0" implementation "org.springframework.boot:spring-boot-starter-aop" + + implementation "org.springframework:spring-webflux" + implementation "io.projectreactor.netty:reactor-netty" + implementation "org.apache.httpcomponents.client5:httpclient5:5.3.1" } \ No newline at end of file diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AIServerAdapter.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AIServerAdapter.java new file mode 100644 index 00000000..9a31c764 --- /dev/null +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AIServerAdapter.java @@ -0,0 +1,88 @@ +package com.icc.qasker.quiz.adapter; + +import com.icc.qasker.global.error.ClientSideException; +import com.icc.qasker.global.error.CustomException; +import com.icc.qasker.global.error.ExceptionMessage; +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClient; + +@Slf4j +@Component +@AllArgsConstructor +public class AIServerAdapter { + + RestClient aiStreamClient; + + @CircuitBreaker(name = "aiServer", fallbackMethod = "fallback") + public void streamRequest(GenerationRequest request, Consumer onLineReceived) { + aiStreamClient.post() + .uri("/generation") + .body(request) + .accept(MediaType.APPLICATION_NDJSON) + .exchange((req, res) -> { + HttpStatusCode status = res.getStatusCode(); + + if (status.is4xxClientError()) { + String messageBody = new String(res.getBody().readAllBytes(), + StandardCharsets.UTF_8); + throw new ClientSideException(messageBody); + } + + if (status.is5xxServerError()) { + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + } + + try ( + InputStream is = res.getBody(); + BufferedReader br = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8)) + ) { + while (true) { + String line = br.readLine(); + if (line == null) { + break; + } + if (line.isBlank()) { + continue; + } + onLineReceived.accept(line); + } + } catch (IOException e) { + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + } + + return null; + }); + } + + private void fallback(GenerationRequest request, Consumer onLineReceived, Throwable t) { + if (t instanceof CallNotPermittedException) { + log.error("⛔ [CircuitBreaker] AI 서버 요청 차단됨 (Circuit Open): {}", t.getMessage()); + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + } + if (t instanceof ResourceAccessException e) { + log.error("⏳ AI 서버 연결 시간 초과/실패: {}", t.getMessage()); + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + } + if (t instanceof ClientSideException e) { + log.error("⏳ 사용자 오류 발생: {}", t.getMessage()); + throw new ClientSideException(t.getMessage()); + } + log.error("⚠ AI Server Unknown Error: {}", t.getMessage()); + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + } +} \ No newline at end of file diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java deleted file mode 100644 index 3861590d..00000000 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.icc.qasker.quiz.adapter; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.icc.qasker.global.error.ClientSideException; -import com.icc.qasker.global.error.CustomException; -import com.icc.qasker.global.error.ExceptionMessage; -import com.icc.qasker.quiz.dto.request.FeGenerationRequest; -import com.icc.qasker.quiz.dto.response.AiGenerationResponse; -import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Component; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestClient; - -@Slf4j -@Component -public class AiServerAdapter { - - private final RestClient aiRestClient; - - public AiServerAdapter(@Qualifier("aiGenerationRestClient") RestClient aiRestClient) { - this.aiRestClient = aiRestClient; - } - - @CircuitBreaker(name = "aiServer") - public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequest) { - try { - return aiRestClient.post() - .uri("/generation") - .body(feGenerationRequest) - .retrieve() - .body(AiGenerationResponse.class); - - // 1. 400 에러 -> 서킷 브레이커가 무시해야 함 (ignoreExceptions) - } catch (HttpClientErrorException e) { - - String messageBody = e.getResponseBodyAsString(); - log.error("[AI Server] Bad Request: Status={}, Body={}", e.getStatusCode(), - messageBody); - - String message = ""; - try { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(messageBody); - - if (rootNode.has("detail")) { - message = rootNode.get("detail").asText(); - } - } catch (Exception exception) { - message = messageBody; - } - - throw new ClientSideException(message); - - // 2. 5xx 에러 (Server Fault) -> 서킷 브레이커가 실패로 기록해야 함 - } catch (HttpServerErrorException e) { - log.error("AI Server Server Error (5xx): Status={}, Body={}", e.getStatusCode(), - e.getResponseBodyAsString()); - // 예외를 그대로 던지거나, 커스텀 예외(ServerException)로 감싸서 던져야 함 - throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); - - // 3. 타임아웃/연결 오류 (Network Fault) -> 서킷 브레이커가 실패로 기록해야 함 - } catch (ResourceAccessException e) { - log.error("AI Server Connection/Timeout Error: {}", e.getMessage()); - // 타임아웃은 반드시 던져야 함 - throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); - - // 4. 그 외의 오류 - } catch (Exception e) { - log.error("AI Server Unknown Error: {}", e.getMessage()); - throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); - } - } -} \ No newline at end of file diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/MockAiServerAdapter.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/MockAiServerAdapter.java deleted file mode 100644 index 4e5b3f0d..00000000 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/MockAiServerAdapter.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.icc.qasker.quiz.adapter; - -import com.icc.qasker.quiz.dto.request.FeGenerationRequest; -import com.icc.qasker.quiz.dto.response.AiGenerationResponse; -import com.icc.qasker.quiz.dto.response.QuizGeneratedByAI; -import com.icc.qasker.quiz.dto.response.QuizGeneratedByAI.SelectionsOfAi; -import java.util.List; -import java.util.stream.IntStream; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -/** - * 부하 테스트용 Mock Adapter 'stress-test' 프로파일이 활성화되었을 때만 빈으로 등록되고, - * - * @Primary를 통해 기존 AiServerAdapter보다 우선순위를 가짐. - */ -@Component -@Profile("stress-test") -@Primary -public class MockAiServerAdapter extends AiServerAdapter { - - public MockAiServerAdapter() { - super(null); // 부모의 RestClient는 필요 없음 - } - - @Override - public AiGenerationResponse requestGenerate(FeGenerationRequest feGenerationRequest) { - // 요청된 퀴즈 개수만큼 더미 데이터 생성 - List quizzes = IntStream.range(0, feGenerationRequest.quizCount()) - .mapToObj(i -> new QuizGeneratedByAI( - i + 1, - "Mock Quiz Title " + i, - List.of( - new SelectionsOfAi("Option 1", true), - new SelectionsOfAi("Option 2", false), - new SelectionsOfAi("Option 3", false), - new SelectionsOfAi("Option 4", false) - ), - "Mock Explanation " + i, - List.of() - )) - .toList(); - - return new AiGenerationResponse(quizzes); - } -} diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/RestClientConfig.java similarity index 56% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java rename to modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/RestClientConfig.java index 2552dbbd..e1e43e26 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/AiWebClientConfig.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/RestClientConfig.java @@ -3,36 +3,46 @@ import com.icc.qasker.global.properties.QAskerProperties; import java.time.Duration; import lombok.AllArgsConstructor; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.util.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestClient; @Configuration @AllArgsConstructor -public class AiWebClientConfig { +public class RestClientConfig { private final QAskerProperties qAskerProperties; @Primary - @Bean("aiGenerationRestClient") - public RestClient aiGenerationRestClient() { - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setConnectTimeout(Duration.ofSeconds(5)); - factory.setReadTimeout(Duration.ofSeconds(80)); + @Bean("aiStreamClient") + public RestClient aiGenerationClient(QAskerProperties qAskerProperties) { + // 1. Apache HttpClient 5 설정 (타임아웃 등) + RequestConfig requestConfig = RequestConfig.custom() + .setResponseTimeout(Timeout.ofSeconds(100)) + .setConnectTimeout(Timeout.ofSeconds(5)) + .build(); + + CloseableHttpClient httpClient = HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .build(); + // 2. RestClient 빌드 return RestClient.builder() .baseUrl(qAskerProperties.getAiServerUrl()) - .requestFactory(factory) - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)) .build(); } - @Primary - @Bean("aiFindRestClient") + @Bean("aiRestClient") public RestClient aiRestClient() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(Duration.ofSeconds(5)); @@ -44,17 +54,4 @@ public RestClient aiRestClient() { .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } - - @Bean("aiMockingRestClient") - public RestClient aiMockingRestClient() { - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setConnectTimeout(Duration.ofSeconds(5)); - factory.setReadTimeout(Duration.ofSeconds(60)); - - return RestClient.builder() - .baseUrl(qAskerProperties.getAiMockingServerUrl()) - .requestFactory(factory) - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .build(); - } } \ No newline at end of file diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ExplanationController.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ExplanationController.java index d8d791d9..c2ac1e81 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ExplanationController.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ExplanationController.java @@ -1,8 +1,8 @@ package com.icc.qasker.quiz.controller; import com.icc.qasker.quiz.ExplanationService; -import com.icc.qasker.quiz.controller.doc.ExplanationApiDoc; -import com.icc.qasker.quiz.dto.response.ExplanationResponse; +import com.icc.qasker.quiz.doc.ExplanationApiDoc; +import com.icc.qasker.quiz.dto.feResponse.ExplanationResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/GenerationController.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/GenerationController.java index 6be7e126..40683f2b 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/GenerationController.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/GenerationController.java @@ -2,17 +2,16 @@ import com.icc.qasker.global.annotation.UserId; import com.icc.qasker.quiz.GenerationService; -import com.icc.qasker.quiz.controller.doc.GenerationApiDoc; -import com.icc.qasker.quiz.dto.request.FeGenerationRequest; -import com.icc.qasker.quiz.dto.response.GenerationResponse; -import com.icc.qasker.quiz.service.MockGenerationService; +import com.icc.qasker.quiz.doc.GenerationApiDoc; +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; +import org.springframework.http.MediaType; 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; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @RestController @RequiredArgsConstructor @@ -20,23 +19,14 @@ public class GenerationController implements GenerationApiDoc { private final GenerationService generationService; - private final MockGenerationService mockGenerationService; - @PostMapping + @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) @Override - public ResponseEntity postProblemSetId( + public SseEmitter postProblemSetId( @UserId String userId, - @Valid @RequestBody FeGenerationRequest feGenerationRequest) { - return ResponseEntity.ok( - generationService.processGenerationRequest(feGenerationRequest, userId)); - } - - @PostMapping("/mock") - @Override - public ResponseEntity generateMockQuiz( - @Valid @RequestBody FeGenerationRequest feGenerationRequest) { - return ResponseEntity.ok( - mockGenerationService.processGenerationRequest(feGenerationRequest)); + @Valid @RequestBody + GenerationRequest generationRequest) { + return generationService.processGenerationRequest(generationRequest, userId); } } diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ProblemSetController.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ProblemSetController.java index 32ff70d8..89212e33 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ProblemSetController.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/ProblemSetController.java @@ -1,8 +1,8 @@ package com.icc.qasker.quiz.controller; import com.icc.qasker.quiz.ProblemSetService; -import com.icc.qasker.quiz.controller.doc.ProblemSetApiDoc; -import com.icc.qasker.quiz.dto.response.ProblemSetResponse; +import com.icc.qasker.quiz.doc.ProblemSetApiDoc; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/SpecificExplanationController.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/SpecificExplanationController.java index 4f4eb72b..166a8e43 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/SpecificExplanationController.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/SpecificExplanationController.java @@ -1,8 +1,8 @@ package com.icc.qasker.quiz.controller; import com.icc.qasker.quiz.SpecificExplanationService; -import com.icc.qasker.quiz.controller.doc.SpecificExplanationApiDoc; -import com.icc.qasker.quiz.dto.response.SpecificExplanationResponse; +import com.icc.qasker.quiz.doc.SpecificExplanationApiDoc; +import com.icc.qasker.quiz.dto.feResponse.SpecificExplanationResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/GenerationApiDoc.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/GenerationApiDoc.java deleted file mode 100644 index f7dcdbe5..00000000 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/GenerationApiDoc.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.icc.qasker.quiz.controller.doc; - -import com.icc.qasker.quiz.dto.request.FeGenerationRequest; -import com.icc.qasker.quiz.dto.response.GenerationResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; - -@Tag(name = "Generation", description = "생성 관련 API") -public interface GenerationApiDoc { - - @Operation(summary = "문제를 생성한다") - @PostMapping - ResponseEntity postProblemSetId( - String userId, - @RequestBody FeGenerationRequest feGenerationRequest); - - - @Operation(summary = "모의 퀴즈 생성을 요청한다") - @PostMapping("/mock") - ResponseEntity generateMockQuiz( - @RequestBody FeGenerationRequest feGenerationRequest); -} diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Problem.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Problem.java index 075c77f7..d6bd10cd 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Problem.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Problem.java @@ -4,7 +4,7 @@ import static java.util.stream.Collectors.toList; import com.icc.qasker.global.entity.CreatedAt; -import com.icc.qasker.quiz.dto.response.QuizGeneratedByAI; +import com.icc.qasker.quiz.dto.aiResponse.QuizGeneratedFromAI; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.EmbeddedId; @@ -43,7 +43,7 @@ public class Problem extends CreatedAt { @OneToMany(mappedBy = "problem", cascade = CascadeType.ALL, orphanRemoval = true) private List referencedPages = new ArrayList<>(); - public static Problem of(QuizGeneratedByAI quizDto, ProblemSet problemSet) { + public static Problem of(QuizGeneratedFromAI quizDto, ProblemSet problemSet) { Problem problem = new Problem(); ProblemId problemId = new ProblemId(); problemId.setNumber(quizDto.getNumber()); diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java index 9e10f36a..88b0691a 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/ProblemSet.java @@ -3,7 +3,7 @@ import com.icc.qasker.global.entity.CreatedAt; import com.icc.qasker.global.error.CustomException; import com.icc.qasker.global.error.ExceptionMessage; -import com.icc.qasker.quiz.dto.response.AiGenerationResponse; +import com.icc.qasker.quiz.dto.aiResponse.GenerationResponseFromAI; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -12,15 +12,14 @@ import jakarta.persistence.OneToMany; import java.util.List; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; @Entity @Getter @NoArgsConstructor @AllArgsConstructor -@Setter public class ProblemSet extends CreatedAt { @@ -32,20 +31,23 @@ public class ProblemSet extends CreatedAt { @OneToMany(mappedBy = "problemSet", cascade = CascadeType.ALL, orphanRemoval = true) private List problems; - public static ProblemSet of(AiGenerationResponse aiResponse) { + @Builder + public ProblemSet(String userId) { + this.userId = userId; + } + + public static ProblemSet of(GenerationResponseFromAI aiResponse) { return of(aiResponse, null); } - public static ProblemSet of(AiGenerationResponse aiResponse, String userId) { + public static ProblemSet of(GenerationResponseFromAI aiResponse, String userId) { if (aiResponse == null || aiResponse.getQuiz() == null) { throw new CustomException(ExceptionMessage.NULL_AI_RESPONSE); } - ProblemSet problemSet = new ProblemSet(); - problemSet.setUserId(userId); - List problems = aiResponse.getQuiz().stream() + ProblemSet problemSet = ProblemSet.builder().userId(userId).build(); + problemSet.problems = aiResponse.getQuiz().stream() .map(quizDto -> Problem.of(quizDto, problemSet)) .toList(); - problemSet.problems = problems; return problemSet; } } diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Selection.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Selection.java index bc9ec152..ef72271b 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Selection.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/Selection.java @@ -3,7 +3,7 @@ import static jakarta.persistence.FetchType.LAZY; import com.icc.qasker.global.entity.CreatedAt; -import com.icc.qasker.quiz.dto.response.QuizGeneratedByAI; +import com.icc.qasker.quiz.dto.aiResponse.QuizGeneratedFromAI; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -39,7 +39,7 @@ public class Selection extends CreatedAt { }) private Problem problem; - public static Selection of(QuizGeneratedByAI.SelectionsOfAi selDto, Problem problem) { + public static Selection of(QuizGeneratedFromAI.SelectionsOfAi selDto, Problem problem) { Selection selection = new Selection(); selection.content = selDto.getContent(); selection.correct = selDto.isCorrect(); diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/ProblemSetResponseMapper.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/ProblemSetResponseMapper.java index 00f3e517..0751a8c7 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/ProblemSetResponseMapper.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/ProblemSetResponseMapper.java @@ -1,27 +1,24 @@ package com.icc.qasker.quiz.mapper; -import com.icc.qasker.quiz.dto.response.ProblemSetResponse; -import com.icc.qasker.quiz.dto.response.ProblemSetResponse.QuizForFe; -import com.icc.qasker.quiz.dto.response.ProblemSetResponse.QuizForFe.SelectionsForFE; +import com.icc.qasker.global.util.HashUtil; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse.QuizForFe; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse.QuizForFe.SelectionsForFE; import com.icc.qasker.quiz.entity.Problem; import com.icc.qasker.quiz.entity.ProblemSet; import com.icc.qasker.quiz.entity.Selection; import java.util.List; import java.util.stream.IntStream; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +@Component +@AllArgsConstructor public final class ProblemSetResponseMapper { - public static ProblemSetResponse fromEntity(ProblemSet problemSet) { - List quizzes = problemSet.getProblems().stream() - .map(ProblemSetResponseMapper::fromEntity) - .toList(); - - return new ProblemSetResponse( - quizzes - ); - } + private final HashUtil hashUtil; - private static QuizForFe fromEntity(Problem problem) { + public QuizForFe fromEntity(Problem problem) { List selections = IntStream .range(0, problem.getSelections().size()) .mapToObj(i -> { @@ -42,4 +39,16 @@ private static QuizForFe fromEntity(Problem problem) { selections ); } + + public ProblemSetResponse fromEntity(ProblemSet problemSet) { + List quizzes = problemSet.getProblems().stream() + .map(this::fromEntity) + .toList(); + + return new ProblemSetResponse( + hashUtil.encode(problemSet.getId()), + quizzes.size(), + quizzes + ); + } } \ No newline at end of file diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/repository/ProblemRepository.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/repository/ProblemRepository.java index 1002a521..725965b4 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/repository/ProblemRepository.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/repository/ProblemRepository.java @@ -11,5 +11,7 @@ public interface ProblemRepository extends JpaRepository { List findByIdProblemSetId(Long problemSetId); Optional findByIdProblemSetIdAndIdNumber(Long problemSetId, int number); + + int countByProblemSetId(Long id); } diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ExplanationServiceImpl.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ExplanationServiceImpl.java index f115dfdf..4f97e682 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ExplanationServiceImpl.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ExplanationServiceImpl.java @@ -4,8 +4,8 @@ import com.icc.qasker.global.error.ExceptionMessage; import com.icc.qasker.global.util.HashUtil; import com.icc.qasker.quiz.ExplanationService; -import com.icc.qasker.quiz.dto.response.ExplanationResponse; -import com.icc.qasker.quiz.dto.response.ResultResponse; +import com.icc.qasker.quiz.dto.feResponse.ExplanationResponse; +import com.icc.qasker.quiz.dto.feResponse.ResultResponse; import com.icc.qasker.quiz.entity.Problem; import com.icc.qasker.quiz.entity.ReferencedPage; import com.icc.qasker.quiz.repository.ProblemRepository; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java index 8fcf1732..c33c7f3c 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java @@ -1,67 +1,172 @@ package com.icc.qasker.quiz.service; -import com.icc.qasker.aws.S3Service; -import com.icc.qasker.aws.S3ValidateService; -import com.icc.qasker.aws.dto.FileExistStatusResponse; -import com.icc.qasker.aws.dto.Status; +import com.fasterxml.jackson.databind.ObjectMapper; import com.icc.qasker.global.component.SlackNotifier; import com.icc.qasker.global.error.CustomException; import com.icc.qasker.global.error.ExceptionMessage; import com.icc.qasker.global.util.HashUtil; import com.icc.qasker.quiz.GenerationService; -import com.icc.qasker.quiz.adapter.AiServerAdapter; -import com.icc.qasker.quiz.dto.request.FeGenerationRequest; -import com.icc.qasker.quiz.dto.response.AiGenerationResponse; -import com.icc.qasker.quiz.dto.response.GenerationResponse; +import com.icc.qasker.quiz.adapter.AIServerAdapter; +import com.icc.qasker.quiz.dto.aiResponse.GenerationResponseFromAI; +import com.icc.qasker.quiz.dto.aiResponse.QuizGeneratedFromAI; +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse.QuizForFe; +import com.icc.qasker.quiz.entity.Problem; import com.icc.qasker.quiz.entity.ProblemSet; +import com.icc.qasker.quiz.mapper.ProblemSetResponseMapper; +import com.icc.qasker.quiz.repository.ProblemRepository; import com.icc.qasker.quiz.repository.ProblemSetRepository; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreaker.State; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +@Slf4j @Service @AllArgsConstructor public class GenerationServiceImpl implements GenerationService { + private final CircuitBreakerRegistry circuitBreakerRegistry; + private final ProblemSetResponseMapper problemSetResponseMapper; private final SlackNotifier slackNotifier; - private final AiServerAdapter aiServerAdapter; private final ProblemSetRepository problemSetRepository; private final HashUtil hashUtil; - private final S3ValidateService s3ValidateService; - private final S3Service s3Service; + private final ProblemRepository problemRepository; + private final AIServerAdapter aiServerAdapter; + private final ObjectMapper objectMapper; @Override - public GenerationResponse processGenerationRequest( - FeGenerationRequest feGenerationRequest, String userId) { - validateQuizCount(feGenerationRequest); - FileExistStatusResponse fileExistStatusResponse = s3Service.checkFileExistence( - feGenerationRequest.uploadedUrl()); - if (fileExistStatusResponse.status().equals(Status.NOT_EXIST)) { - throw new CustomException(ExceptionMessage.FILE_NOT_FOUND_ON_S3); + public SseEmitter processGenerationRequest( + GenerationRequest request, String userId) { + SseEmitter emitter = new SseEmitter(110 * 1000L); + + CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("aiServer"); + if (circuitBreaker.getState() == State.OPEN) { + sendErrorAndComplete(emitter, + new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR)); + return emitter; } - s3ValidateService.checkCloudFrontUrlWithThrowing(feGenerationRequest.uploadedUrl()); - AiGenerationResponse aiResponse = aiServerAdapter.requestGenerate(feGenerationRequest); + ProblemSet saveProblemSet = null; + try { + ProblemSet problemSet = ProblemSet.builder().userId(userId).build(); + saveProblemSet = problemSetRepository.save(problemSet); + } catch (Exception e) { + log.error("초기 저장 실패: {}", e.getMessage()); + sendErrorAndComplete(emitter, e); + } - ProblemSet problemSet = ProblemSet.of(aiResponse, userId); - ProblemSet savedPs = problemSetRepository.save(problemSet); + ProblemSet finalSaveProblemSet = saveProblemSet; + Thread.ofVirtual().start(() -> { + try { + aiServerAdapter.streamRequest( + request, + (line) -> doMainLogic(request, line, emitter, finalSaveProblemSet) + ); + finalizeSuccess(request, finalSaveProblemSet.getId(), emitter); + } catch (Exception e) { + finalizeError(request, finalSaveProblemSet.getId(), emitter, e); + } + }); + return emitter; + } - GenerationResponse response = new GenerationResponse( - hashUtil.encode(savedPs.getId()) - ); + private void doMainLogic(GenerationRequest request, String line, SseEmitter emitter, + ProblemSet saveProblemSet) { + try { + String encodedId = hashUtil.encode(saveProblemSet.getId()); + List problems = new ArrayList<>(); + List quizForFeList = new ArrayList<>(); + GenerationResponseFromAI dto = objectMapper.readValue(line, + GenerationResponseFromAI.class); + for (QuizGeneratedFromAI quizGeneratedFromAI : dto.getQuiz()) { + Problem problem = Problem.of(quizGeneratedFromAI, saveProblemSet); + problems.add(problem); + quizForFeList.add(problemSetResponseMapper.fromEntity(problem)); + } + problemRepository.saveAll(problems); + emitter.send( + new ProblemSetResponse( + encodedId, + request.quizCount(), + quizForFeList + )); + } catch (IOException ignored) { + } + } - slackNotifier.notifyText(""" + private void finalizeSuccess( + GenerationRequest request, + Long problemSetId, + SseEmitter emitter + ) { + String encodedId = hashUtil.encode(problemSetId); + slackNotifier.asyncNotifyText(""" ✅ [퀴즈 생성 완료 알림] - ProblemSet ID: %s + ProblemSetId: %s + 퀴즈 타입: %s + 문제 수: %d """.formatted( - response.getProblemSetId() + encodedId, + request.quizType(), + request.quizCount() )); + emitter.complete(); + } + + private void finalizeError( + GenerationRequest request, + Long problemSetId, + SseEmitter emitter, + Exception e + ) { + + int generatedCount = problemRepository.countByProblemSetId(problemSetId); + if (generatedCount > 0) { + log.warn("⚠ 퀴즈 생성 중 오류 발생, 일부 데이터({}개)는 보존됨, Error: {}", + generatedCount, + e.getMessage() + ); - return response; + emitter.complete(); + + slackNotifier.asyncNotifyText(""" + ⚠️ [퀴즈 생성 부분 완료] + ProblemSetId: %s + 생성된 문제 수: %d개 중 %d개 + 시유: 생성 중단 (%s) + """.formatted( + hashUtil.encode(problemSetId), + request.quizCount(), + generatedCount, + e.getMessage() + )); + } else { + sendErrorAndComplete(emitter, e); + // 영속성 컨텍스트 추가 -> 삭제 비용 + // problemSetRepository.delete(problemSet); + slackNotifier.asyncNotifyText(""" + ❌ [퀴즈 생성 실패] + 사유: %s + """.formatted( + e.getMessage() + )); + problemSetRepository.deleteById(problemSetId); + } } - private void validateQuizCount(FeGenerationRequest feGenerationRequest) { - if (feGenerationRequest.quizCount() % 5 != 0) { - throw new CustomException(ExceptionMessage.INVALID_QUIZ_COUNT_REQUEST); + private void sendErrorAndComplete(SseEmitter emitter, Exception e) { + try { + emitter.send(SseEmitter.event().name("error").data(e.getMessage())); + emitter.complete(); + } catch (IOException ignored) { } } -} \ No newline at end of file +} diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/MockGenerationService.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/MockGenerationService.java deleted file mode 100644 index 3373865f..00000000 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/MockGenerationService.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.icc.qasker.quiz.service; - -import com.icc.qasker.global.error.CustomException; -import com.icc.qasker.global.error.ExceptionMessage; -import com.icc.qasker.global.util.HashUtil; -import com.icc.qasker.quiz.dto.request.FeGenerationRequest; -import com.icc.qasker.quiz.dto.response.AiGenerationResponse; -import com.icc.qasker.quiz.dto.response.GenerationResponse; -import com.icc.qasker.quiz.entity.ProblemSet; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestClient; - -@Slf4j -@Service -public class MockGenerationService { - - private static final int DUMMY_PROBLEM_SET_ID = 1; - private final RestClient aiRestClient; - private final HashUtil hashUtil; - - public MockGenerationService( - HashUtil hashUtil, - @Qualifier("aiMockingRestClient") RestClient aiRestClient1) { - this.hashUtil = hashUtil; - this.aiRestClient = aiRestClient1; - } - - public GenerationResponse processGenerationRequest( - FeGenerationRequest feGenerationRequest) { - AiGenerationResponse aiResponse = callAiServer(feGenerationRequest); - - ProblemSet.of(aiResponse); - - return new GenerationResponse( - hashUtil.encode(DUMMY_PROBLEM_SET_ID) - ); - } - - - private AiGenerationResponse callAiServer(FeGenerationRequest feGenerationRequest) { - try { - return aiRestClient.post() - .uri("/generation") - .body(feGenerationRequest) - .retrieve() - .body(AiGenerationResponse.class); - } catch (HttpClientErrorException.TooManyRequests e) { - throw new CustomException(ExceptionMessage.AI_SERVER_TO_MANY_REQUEST); - } catch (ResourceAccessException e) { - if (e.getCause() instanceof java.net.SocketTimeoutException) { - throw new CustomException(ExceptionMessage.AI_SERVER_TIMEOUT); - } - throw new CustomException(ExceptionMessage.AI_SERVER_CONNECTION_FAILED); - } catch (Exception e) { - throw new CustomException(ExceptionMessage.AI_SERVER_RESPONSE_ERROR); - } - } -} diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ProblemSetServiceImpl.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ProblemSetServiceImpl.java index 58be2f0c..d9b2d1b6 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ProblemSetServiceImpl.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/ProblemSetServiceImpl.java @@ -4,7 +4,7 @@ import com.icc.qasker.global.error.ExceptionMessage; import com.icc.qasker.global.util.HashUtil; import com.icc.qasker.quiz.ProblemSetService; -import com.icc.qasker.quiz.dto.response.ProblemSetResponse; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; import com.icc.qasker.quiz.entity.ProblemSet; import com.icc.qasker.quiz.mapper.ProblemSetResponseMapper; import com.icc.qasker.quiz.repository.ProblemSetRepository; @@ -16,6 +16,7 @@ @AllArgsConstructor public class ProblemSetServiceImpl implements ProblemSetService { + private final ProblemSetResponseMapper problemSetResponseMapper; private final ProblemSetRepository problemSetRepository; private final HashUtil hashUtil; @@ -25,7 +26,7 @@ public ProblemSetResponse getProblemSet(String problemSetId) { long id = hashUtil.decode(problemSetId); ProblemSet problemSet = getProblemSetEntity(id); - return toResponse(problemSet); + return problemSetResponseMapper.fromEntity(problemSet); } private ProblemSet getProblemSetEntity(long id) { @@ -34,9 +35,5 @@ private ProblemSet getProblemSetEntity(long id) { () -> new CustomException(ExceptionMessage.PROBLEM_SET_NOT_FOUND) ); } - - private ProblemSetResponse toResponse(ProblemSet problemSet) { - return ProblemSetResponseMapper.fromEntity(problemSet); - } } diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/SpecificExplanationServiceImpl.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/SpecificExplanationServiceImpl.java index 6ea2e7a6..c276d81a 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/SpecificExplanationServiceImpl.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/SpecificExplanationServiceImpl.java @@ -7,9 +7,9 @@ import com.icc.qasker.global.error.ExceptionMessage; import com.icc.qasker.global.util.HashUtil; import com.icc.qasker.quiz.SpecificExplanationService; -import com.icc.qasker.quiz.dto.request.SpecificExplanationRequest; -import com.icc.qasker.quiz.dto.response.QuizGeneratedByAI.SelectionsOfAi; -import com.icc.qasker.quiz.dto.response.SpecificExplanationResponse; +import com.icc.qasker.quiz.dto.aiRequest.SpecificExplanationRequestToAI; +import com.icc.qasker.quiz.dto.aiResponse.QuizGeneratedFromAI.SelectionsOfAi; +import com.icc.qasker.quiz.dto.feResponse.SpecificExplanationResponse; import com.icc.qasker.quiz.entity.Problem; import com.icc.qasker.quiz.repository.ProblemRepository; import java.util.Objects; @@ -30,7 +30,7 @@ public class SpecificExplanationServiceImpl implements SpecificExplanationServic private final ProblemRepository problemRepository; public SpecificExplanationServiceImpl( - @Qualifier("aiFindRestClient") RestClient aiRestClient, + @Qualifier("aiRestClient") RestClient aiRestClient, ProblemRepository problemRepository, HashUtil hashUtil ) { @@ -46,7 +46,7 @@ public SpecificExplanationResponse getSpecificExplanation(String encodedProblemS Long problemSetId = hashUtil.decode(encodedProblemSetId); Problem problem = problemRepository.findByIdProblemSetIdAndIdNumber(problemSetId, number) .orElseThrow(() -> new CustomException(ExceptionMessage.PROBLEM_SET_NOT_FOUND)); - SpecificExplanationRequest aiRequest = new SpecificExplanationRequest( + SpecificExplanationRequestToAI aiRequest = new SpecificExplanationRequestToAI( problem.getTitle(), problem.getSelections().stream().map(selection -> { SelectionsOfAi s = new SelectionsOfAi(); diff --git a/modules/util/api/build.gradle b/modules/util/api/build.gradle new file mode 100644 index 00000000..620edb71 --- /dev/null +++ b/modules/util/api/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'java-library' +} \ No newline at end of file diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/UpdateLogApiDocs.java b/modules/util/api/src/main/java/com/icc/qasker/util/doc/UpdateLogApiDocs.java similarity index 82% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/UpdateLogApiDocs.java rename to modules/util/api/src/main/java/com/icc/qasker/util/doc/UpdateLogApiDocs.java index 1dd1472f..a43cb78e 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/UpdateLogApiDocs.java +++ b/modules/util/api/src/main/java/com/icc/qasker/util/doc/UpdateLogApiDocs.java @@ -1,8 +1,8 @@ -package com.icc.qasker.quiz.controller.doc; +package com.icc.qasker.util.doc; -import com.icc.qasker.quiz.dto.request.UpdateLogRequest; -import com.icc.qasker.quiz.dto.response.UpdateLogResponse; +import com.icc.qasker.util.dto.request.UpdateLogRequest; +import com.icc.qasker.util.dto.response.UpdateLogResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; @@ -20,5 +20,4 @@ public interface UpdateLogApiDocs { @Operation(summary = "변경사항 업데이트를 보낸다") @PostMapping ResponseEntity createUpdateLog(@RequestBody UpdateLogRequest request); - } diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/UpdateLogRequest.java b/modules/util/api/src/main/java/com/icc/qasker/util/dto/request/UpdateLogRequest.java similarity index 57% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/UpdateLogRequest.java rename to modules/util/api/src/main/java/com/icc/qasker/util/dto/request/UpdateLogRequest.java index d1e21aa7..ec7e2735 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/request/UpdateLogRequest.java +++ b/modules/util/api/src/main/java/com/icc/qasker/util/dto/request/UpdateLogRequest.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.request; +package com.icc.qasker.util.dto.request; public record UpdateLogRequest(String updateText) { diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/UpdateLogResponse.java b/modules/util/api/src/main/java/com/icc/qasker/util/dto/response/UpdateLogResponse.java similarity index 81% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/UpdateLogResponse.java rename to modules/util/api/src/main/java/com/icc/qasker/util/dto/response/UpdateLogResponse.java index 63364852..50293111 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/UpdateLogResponse.java +++ b/modules/util/api/src/main/java/com/icc/qasker/util/dto/response/UpdateLogResponse.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.dto.response; +package com.icc.qasker.util.dto.response; import java.time.Instant; import java.util.List; diff --git a/modules/util/impl/build.gradle b/modules/util/impl/build.gradle new file mode 100644 index 00000000..946b6a7f --- /dev/null +++ b/modules/util/impl/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java-library' +} + +dependencies { + implementation project(":global") + implementation project(":util:util-api") +} \ No newline at end of file diff --git a/app/src/main/java/com/icc/qasker/HelloController.java b/modules/util/impl/src/main/java/com/icc/qasker/util/controller/HelloController.java similarity index 93% rename from app/src/main/java/com/icc/qasker/HelloController.java rename to modules/util/impl/src/main/java/com/icc/qasker/util/controller/HelloController.java index d54958ca..bc4640cd 100644 --- a/app/src/main/java/com/icc/qasker/HelloController.java +++ b/modules/util/impl/src/main/java/com/icc/qasker/util/controller/HelloController.java @@ -1,4 +1,4 @@ -package com.icc.qasker; +package com.icc.qasker.util.controller; import java.util.HashMap; import java.util.Map; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/UpdateLogController.java b/modules/util/impl/src/main/java/com/icc/qasker/util/controller/UpdateLogController.java similarity index 73% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/UpdateLogController.java rename to modules/util/impl/src/main/java/com/icc/qasker/util/controller/UpdateLogController.java index ec08a958..bc70b125 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/UpdateLogController.java +++ b/modules/util/impl/src/main/java/com/icc/qasker/util/controller/UpdateLogController.java @@ -1,9 +1,8 @@ -package com.icc.qasker.quiz.controller; +package com.icc.qasker.util.controller; -import com.icc.qasker.quiz.controller.doc.UpdateLogApiDocs; -import com.icc.qasker.quiz.dto.request.UpdateLogRequest; -import com.icc.qasker.quiz.dto.response.UpdateLogResponse; -import com.icc.qasker.quiz.service.UpdateLogService; +import com.icc.qasker.util.doc.UpdateLogApiDocs; +import com.icc.qasker.util.dto.response.UpdateLogResponse; +import com.icc.qasker.util.service.UpdateLogService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -24,9 +23,10 @@ public ResponseEntity getUpdateLog() { return ResponseEntity.ok(updateService.getUpdateLog()); } + @PostMapping public ResponseEntity createUpdateLog( - @RequestBody UpdateLogRequest request) { + @RequestBody com.icc.qasker.util.dto.request.UpdateLogRequest request) { return ResponseEntity.ok(updateService.createUpdateLog(request)); } } diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/UpdateLog.java b/modules/util/impl/src/main/java/com/icc/qasker/util/entity/UpdateLog.java similarity index 94% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/UpdateLog.java rename to modules/util/impl/src/main/java/com/icc/qasker/util/entity/UpdateLog.java index a48a94ce..0b582cf9 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/entity/UpdateLog.java +++ b/modules/util/impl/src/main/java/com/icc/qasker/util/entity/UpdateLog.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.entity; +package com.icc.qasker.util.entity; import com.icc.qasker.global.entity.CreatedAt; import jakarta.persistence.Column; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/UpdateLogResponseMapper.java b/modules/util/impl/src/main/java/com/icc/qasker/util/mapper/UpdateLogResponseMapper.java similarity index 75% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/UpdateLogResponseMapper.java rename to modules/util/impl/src/main/java/com/icc/qasker/util/mapper/UpdateLogResponseMapper.java index 4aec06b0..c8642665 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/mapper/UpdateLogResponseMapper.java +++ b/modules/util/impl/src/main/java/com/icc/qasker/util/mapper/UpdateLogResponseMapper.java @@ -1,7 +1,8 @@ -package com.icc.qasker.quiz.mapper; +package com.icc.qasker.util.mapper; -import com.icc.qasker.quiz.dto.response.UpdateLogResponse; -import com.icc.qasker.quiz.entity.UpdateLog; + +import com.icc.qasker.util.dto.response.UpdateLogResponse; +import com.icc.qasker.util.entity.UpdateLog; import java.util.List; public final class UpdateLogResponseMapper { diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/repository/UpdateLogRepository.java b/modules/util/impl/src/main/java/com/icc/qasker/util/repository/UpdateLogRepository.java similarity index 72% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/repository/UpdateLogRepository.java rename to modules/util/impl/src/main/java/com/icc/qasker/util/repository/UpdateLogRepository.java index 4be4740d..8eae1177 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/repository/UpdateLogRepository.java +++ b/modules/util/impl/src/main/java/com/icc/qasker/util/repository/UpdateLogRepository.java @@ -1,6 +1,6 @@ -package com.icc.qasker.quiz.repository; +package com.icc.qasker.util.repository; -import com.icc.qasker.quiz.entity.UpdateLog; +import com.icc.qasker.util.entity.UpdateLog; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/UpdateLogService.java b/modules/util/impl/src/main/java/com/icc/qasker/util/service/UpdateLogService.java similarity index 70% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/UpdateLogService.java rename to modules/util/impl/src/main/java/com/icc/qasker/util/service/UpdateLogService.java index d8eaf88d..d8ca8e8e 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/UpdateLogService.java +++ b/modules/util/impl/src/main/java/com/icc/qasker/util/service/UpdateLogService.java @@ -1,10 +1,11 @@ -package com.icc.qasker.quiz.service; +package com.icc.qasker.util.service; -import com.icc.qasker.quiz.dto.request.UpdateLogRequest; -import com.icc.qasker.quiz.dto.response.UpdateLogResponse; -import com.icc.qasker.quiz.entity.UpdateLog; -import com.icc.qasker.quiz.mapper.UpdateLogResponseMapper; -import com.icc.qasker.quiz.repository.UpdateLogRepository; + +import com.icc.qasker.util.dto.request.UpdateLogRequest; +import com.icc.qasker.util.dto.response.UpdateLogResponse; +import com.icc.qasker.util.entity.UpdateLog; +import com.icc.qasker.util.mapper.UpdateLogResponseMapper; +import com.icc.qasker.util.repository.UpdateLogRepository; import lombok.AllArgsConstructor; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; @@ -26,7 +27,8 @@ public UpdateLogResponse getUpdateLog() { @Transactional @CachePut(value = "recentUpdateLog", key = "'root'") - public UpdateLogResponse createUpdateLog(UpdateLogRequest request) { + public UpdateLogResponse createUpdateLog( + UpdateLogRequest request) { updateLogRepository.save(UpdateLog.builder() .updateText(request.updateText()) .build()); diff --git a/settings.gradle b/settings.gradle index 423c6734..242b8125 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,8 @@ rootProject.name = 'q-asker' include ':app', ':global', ':auth:auth-api', ':auth:auth-impl', ':aws:aws-api', ':aws:aws-impl', - ':quiz:quiz-api', ':quiz:quiz-impl' + ':quiz:quiz-api', ':quiz:quiz-impl', + ':util:util-api', ':util:util-impl' project(':app').projectDir = file('app') project(':global').projectDir = file('modules/global') @@ -16,3 +17,6 @@ project(':aws:aws-impl').projectDir = file('modules/aws/impl') project(':quiz:quiz-api').projectDir = file('modules/quiz/api') project(':quiz:quiz-impl').projectDir = file('modules/quiz/impl') + +project(':util:util-api').projectDir = file('modules/util/api') +project(':util:util-impl').projectDir = file('modules/util/impl')