From 9f1b29f829f22966a84033d20e9418f5db499786 Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:36:40 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[ICC-232]=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 1 + .../java/com/icc/qasker/aws/S3Service.java | 13 --- .../java/com/icc/qasker/util/S3Service.java | 13 +++ .../{aws => util}/S3ValidateService.java | 2 +- .../com/icc/qasker/util}/doc/S3ApiDoc.java | 8 +- .../dto/FileExistStatusResponse.java | 2 +- .../{aws => util}/dto/PresignRequest.java | 2 +- .../{aws => util}/dto/PresignResponse.java | 2 +- .../icc/qasker/{aws => util}/dto/Status.java | 2 +- .../config/MockS3ClientConfig.java | 2 +- .../{aws => util}/config/S3ClientConfig.java | 4 +- .../controller/S3Controller.java | 12 +- .../properties/AwsCloudFrontProperties.java | 2 +- .../properties/AwsS3Properties.java | 2 +- .../properties/LibreOfficeProperties.java | 2 +- .../{aws => util}/service/S3ServiceImpl.java | 20 ++-- .../service/S3ValidateServiceImpl.java | 8 +- modules/quiz/api/build.gradle | 4 + .../icc/qasker/quiz/ExplanationService.java | 2 +- .../icc/qasker/quiz/GenerationService.java | 7 +- .../icc/qasker/quiz/ProblemSetService.java | 2 +- .../quiz/SpecificExplanationService.java | 2 +- .../qasker/quiz}/doc/ExplanationApiDoc.java | 4 +- .../icc/qasker/quiz/doc/GenerationApiDoc.java | 19 +++ .../qasker/quiz}/doc/ProblemSetApiDoc.java | 4 +- .../quiz}/doc/SpecificExplanationApiDoc.java | 2 +- .../dto/aiRequest/GenerationRequestToAI.java | 29 +++++ .../SpecificExplanationRequestToAI.java} | 6 +- .../GenerationResponseFromAI.java} | 6 +- .../QuizGeneratedFromAI.java} | 4 +- .../GenerationRequest.java} | 8 +- .../enums/DifficultyType.java | 2 +- .../enums/QuizType.java | 2 +- .../ExplanationResponse.java | 2 +- .../GenerationResponse.java | 2 +- .../ProblemSetResponse.java | 2 +- .../ResultResponse.java | 2 +- .../SpecificExplanationResponse.java | 2 +- modules/quiz/impl/build.gradle | 1 + .../qasker/quiz/adapter/AiServerAdapter.java | 108 ++++++++++-------- .../quiz/adapter/MockAiServerAdapter.java | 47 -------- .../qasker/quiz/config/AiWebClientConfig.java | 28 +---- .../controller/ExplanationController.java | 4 +- .../quiz/controller/GenerationController.java | 28 ++--- .../quiz/controller/ProblemSetController.java | 4 +- .../SpecificExplanationController.java | 4 +- .../quiz/controller/doc/GenerationApiDoc.java | 25 ---- .../com/icc/qasker/quiz/entity/Problem.java | 4 +- .../icc/qasker/quiz/entity/ProblemSet.java | 20 ++-- .../com/icc/qasker/quiz/entity/Selection.java | 4 +- .../quiz/mapper/ProblemSetResponseMapper.java | 6 +- .../quiz/service/ExplanationServiceImpl.java | 4 +- .../quiz/service/GenerationServiceImpl.java | 41 +++---- .../quiz/service/MockGenerationService.java | 62 ---------- .../quiz/service/ProblemSetServiceImpl.java | 2 +- .../SpecificExplanationServiceImpl.java | 10 +- modules/util/api/build.gradle | 3 + .../qasker/util}/doc/UpdateLogApiDocs.java | 7 +- .../util}/dto/request/UpdateLogRequest.java | 2 +- .../util}/dto/response/UpdateLogResponse.java | 2 +- modules/util/impl/build.gradle | 8 ++ .../util/controller}/HelloController.java | 2 +- .../util}/controller/UpdateLogController.java | 12 +- .../icc/qasker/util}/entity/UpdateLog.java | 2 +- .../util}/mapper/UpdateLogResponseMapper.java | 7 +- .../util}/repository/UpdateLogRepository.java | 4 +- .../util}/service/UpdateLogService.java | 16 +-- settings.gradle | 6 +- 68 files changed, 300 insertions(+), 383 deletions(-) delete mode 100644 modules/aws/api/src/main/java/com/icc/qasker/aws/S3Service.java create mode 100644 modules/aws/api/src/main/java/com/icc/qasker/util/S3Service.java rename modules/aws/api/src/main/java/com/icc/qasker/{aws => util}/S3ValidateService.java (86%) rename modules/aws/{impl/src/main/java/com/icc/qasker/aws/controller => api/src/main/java/com/icc/qasker/util}/doc/S3ApiDoc.java (83%) rename modules/aws/api/src/main/java/com/icc/qasker/{aws => util}/dto/FileExistStatusResponse.java (66%) rename modules/aws/api/src/main/java/com/icc/qasker/{aws => util}/dto/PresignRequest.java (93%) rename modules/aws/api/src/main/java/com/icc/qasker/{aws => util}/dto/PresignResponse.java (75%) rename modules/aws/api/src/main/java/com/icc/qasker/{aws => util}/dto/Status.java (59%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/config/MockS3ClientConfig.java (97%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/config/S3ClientConfig.java (94%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/controller/S3Controller.java (80%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/properties/AwsCloudFrontProperties.java (82%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/properties/AwsS3Properties.java (89%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/properties/LibreOfficeProperties.java (83%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/service/S3ServiceImpl.java (91%) rename modules/aws/impl/src/main/java/com/icc/qasker/{aws => util}/service/S3ValidateServiceImpl.java (88%) rename modules/quiz/{impl/src/main/java/com/icc/qasker/quiz/controller => api/src/main/java/com/icc/qasker/quiz}/doc/ExplanationApiDoc.java (83%) create mode 100644 modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/GenerationApiDoc.java rename modules/quiz/{impl/src/main/java/com/icc/qasker/quiz/controller => api/src/main/java/com/icc/qasker/quiz}/doc/ProblemSetApiDoc.java (84%) rename modules/quiz/{impl/src/main/java/com/icc/qasker/quiz/controller => api/src/main/java/com/icc/qasker/quiz}/doc/SpecificExplanationApiDoc.java (79%) create mode 100644 modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/aiRequest/GenerationRequestToAI.java rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{request/SpecificExplanationRequest.java => aiRequest/SpecificExplanationRequestToAI.java} (61%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{response/AiGenerationResponse.java => aiResponse/GenerationResponseFromAI.java} (70%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{response/QuizGeneratedByAI.java => aiResponse/QuizGeneratedFromAI.java} (92%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{request/FeGenerationRequest.java => feRequest/GenerationRequest.java} (81%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{request => feRequest}/enums/DifficultyType.java (58%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{request => feRequest}/enums/QuizType.java (54%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{response => feResponse}/ExplanationResponse.java (81%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{response => feResponse}/GenerationResponse.java (78%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{response => feResponse}/ProblemSetResponse.java (94%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{response => feResponse}/ResultResponse.java (84%) rename modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/{response => feResponse}/SpecificExplanationResponse.java (85%) delete mode 100644 modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/MockAiServerAdapter.java delete mode 100644 modules/quiz/impl/src/main/java/com/icc/qasker/quiz/controller/doc/GenerationApiDoc.java delete mode 100644 modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/MockGenerationService.java create mode 100644 modules/util/api/build.gradle rename modules/{quiz/impl/src/main/java/com/icc/qasker/quiz/controller => util/api/src/main/java/com/icc/qasker/util}/doc/UpdateLogApiDocs.java (82%) rename modules/{quiz/api/src/main/java/com/icc/qasker/quiz => util/api/src/main/java/com/icc/qasker/util}/dto/request/UpdateLogRequest.java (57%) rename modules/{quiz/api/src/main/java/com/icc/qasker/quiz => util/api/src/main/java/com/icc/qasker/util}/dto/response/UpdateLogResponse.java (81%) create mode 100644 modules/util/impl/build.gradle rename {app/src/main/java/com/icc/qasker => modules/util/impl/src/main/java/com/icc/qasker/util/controller}/HelloController.java (93%) rename modules/{quiz/impl/src/main/java/com/icc/qasker/quiz => util/impl/src/main/java/com/icc/qasker/util}/controller/UpdateLogController.java (73%) rename modules/{quiz/impl/src/main/java/com/icc/qasker/quiz => util/impl/src/main/java/com/icc/qasker/util}/entity/UpdateLog.java (94%) rename modules/{quiz/impl/src/main/java/com/icc/qasker/quiz => util/impl/src/main/java/com/icc/qasker/util}/mapper/UpdateLogResponseMapper.java (75%) rename modules/{quiz/impl/src/main/java/com/icc/qasker/quiz => util/impl/src/main/java/com/icc/qasker/util}/repository/UpdateLogRepository.java (72%) rename modules/{quiz/impl/src/main/java/com/icc/qasker/quiz => util/impl/src/main/java/com/icc/qasker/util}/service/UpdateLogService.java (70%) diff --git a/app/build.gradle b/app/build.gradle index 84fe7760..45a4855a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ dependencies { implementation project(':auth:auth-impl') implementation project(':aws:aws-impl') implementation project(':quiz:quiz-impl') + implementation project(':util:util-impl') 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/quiz/api/build.gradle b/modules/quiz/api/build.gradle index 620edb71..03d063be 100644 --- a/modules/quiz/api/build.gradle +++ b/modules/quiz/api/build.gradle @@ -1,3 +1,7 @@ plugins { id 'java-library' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-webflux' } \ 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..3fb8a47d 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,12 @@ 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 com.icc.qasker.quiz.dto.feResponse.GenerationResponse; +import reactor.core.publisher.Flux; public interface GenerationService { - GenerationResponse processGenerationRequest(FeGenerationRequest feGenerationRequest, + Flux 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..f5ee4eca --- /dev/null +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/doc/GenerationApiDoc.java @@ -0,0 +1,19 @@ +package com.icc.qasker.quiz.doc; + +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; +import com.icc.qasker.quiz.dto.feResponse.GenerationResponse; +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 reactor.core.publisher.Flux; + +@Tag(name = "Generation", description = "생성 관련 API") +public interface GenerationApiDoc { + + @Operation(summary = "문제를 생성한다") + @PostMapping + Flux postProblemSetId( + 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/GenerationResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/GenerationResponse.java similarity index 78% rename from modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/response/GenerationResponse.java rename to modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/GenerationResponse.java index 00825f2e..41ad0a4a 100644 --- 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/feResponse/GenerationResponse.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/ProblemSetResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java similarity index 94% 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..8f629dc3 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; 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/impl/build.gradle b/modules/quiz/impl/build.gradle index fad8e322..f8f1e457 100644 --- a/modules/quiz/impl/build.gradle +++ b/modules/quiz/impl/build.gradle @@ -9,4 +9,5 @@ dependencies { 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.boot:spring-boot-starter-webflux' } \ 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 index 3861590d..3c60b6d8 100644 --- 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 @@ -5,74 +5,82 @@ 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 com.icc.qasker.quiz.dto.aiResponse.GenerationResponseFromAI; +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import java.time.Duration; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; 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; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; @Slf4j @Component public class AiServerAdapter { - private final RestClient aiRestClient; + private final WebClient webClient; - public AiServerAdapter(@Qualifier("aiGenerationRestClient") RestClient aiRestClient) { - this.aiRestClient = aiRestClient; + public AiServerAdapter( + @Qualifier("aiStreamClient") WebClient webClient + ) { + this.webClient = webClient; } @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) { + public Flux requestGenerate(GenerationRequest generationRequest) { + return webClient.post() + .uri("/generation") + .accept(MediaType.APPLICATION_NDJSON) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(generationRequest) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> + clientResponse.bodyToMono(String.class) + .flatMap( + errorBody -> { + String message = parseErrorMessage(errorBody); + log.error("[AI Server] 4xx Error: {}", message); - String messageBody = e.getResponseBodyAsString(); - log.error("[AI Server] Bad Request: Status={}, Body={}", e.getStatusCode(), - messageBody); + return Mono.error(new ClientSideException(message)); + } + ) + ) + .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> + clientResponse.bodyToMono(String.class) + .flatMap( + errorBody -> { + String message = parseErrorMessage(errorBody); + log.error("[AI Server] 5xx Error: {}", message); - String message = ""; - try { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(messageBody); - - if (rootNode.has("detail")) { - message = rootNode.get("detail").asText(); + return Mono.error(new CustomException( + ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR)); + } + ) + ) + .bodyToFlux(GenerationResponseFromAI.class) + .timeout(Duration.ofSeconds(30)) + .doOnError(e -> { + if (!(e instanceof ClientSideException) && !(e instanceof CustomException)) { + log.error("AI Server Network/Timeout Error: {}", e.getMessage()); } - } 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); + private String parseErrorMessage(String messageBody) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(messageBody); + if (rootNode.has("detail")) { + return rootNode.get("detail").asText(); + } + } catch (Exception ignored) { - // 4. 그 외의 오류 - } catch (Exception e) { - log.error("AI Server Unknown Error: {}", e.getMessage()); - throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); } + return messageBody; } -} \ 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/AiWebClientConfig.java index 2552dbbd..ee8d1347 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/AiWebClientConfig.java @@ -10,6 +10,7 @@ import org.springframework.http.MediaType; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; @Configuration @AllArgsConstructor @@ -18,21 +19,15 @@ public class AiWebClientConfig { private final QAskerProperties qAskerProperties; @Primary - @Bean("aiGenerationRestClient") - public RestClient aiGenerationRestClient() { - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setConnectTimeout(Duration.ofSeconds(5)); - factory.setReadTimeout(Duration.ofSeconds(80)); - - return RestClient.builder() + @Bean("aiStreamClient") + public WebClient aiGenerationClient() { + return WebClient.builder() .baseUrl(qAskerProperties.getAiServerUrl()) - .requestFactory(factory) - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } @Primary - @Bean("aiFindRestClient") + @Bean("aiRestClient") public RestClient aiRestClient() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(Duration.ofSeconds(5)); @@ -44,17 +39,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..ea91b64f 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,17 @@ 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 com.icc.qasker.quiz.dto.feResponse.GenerationResponse; 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 reactor.core.publisher.Flux; @RestController @RequiredArgsConstructor @@ -20,23 +20,13 @@ public class GenerationController implements GenerationApiDoc { private final GenerationService generationService; - private final MockGenerationService mockGenerationService; - @PostMapping + @PostMapping(produces = MediaType.APPLICATION_NDJSON_VALUE) @Override - public ResponseEntity postProblemSetId( + public Flux 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..f4cae7dc 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,8 +1,8 @@ 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.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; 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..27b2b373 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,22 +1,23 @@ 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.icc.qasker.util.S3Service; +import com.icc.qasker.util.S3ValidateService; +import com.icc.qasker.util.dto.FileExistStatusResponse; +import com.icc.qasker.util.dto.Status; 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.dto.aiResponse.GenerationResponseFromAI; +import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; +import com.icc.qasker.quiz.dto.feResponse.GenerationResponse; import com.icc.qasker.quiz.entity.ProblemSet; import com.icc.qasker.quiz.repository.ProblemSetRepository; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; @Service @AllArgsConstructor @@ -30,21 +31,15 @@ public class GenerationServiceImpl implements GenerationService { private final S3Service s3Service; @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); - } - s3ValidateService.checkCloudFrontUrlWithThrowing(feGenerationRequest.uploadedUrl()); - - AiGenerationResponse aiResponse = aiServerAdapter.requestGenerate(feGenerationRequest); + public Flux processGenerationRequest( + GenerationRequest request, String userId) { + validateFile(request.uploadedUrl()); - ProblemSet problemSet = ProblemSet.of(aiResponse, userId); + ProblemSet problemSet = ProblemSet.builder().userId(userId).build(); ProblemSet savedPs = problemSetRepository.save(problemSet); + Flux aiResponse = aiServerAdapter.requestGenerate(request); + GenerationResponse response = new GenerationResponse( hashUtil.encode(savedPs.getId()) ); @@ -59,9 +54,11 @@ public GenerationResponse processGenerationRequest( return response; } - private void validateQuizCount(FeGenerationRequest feGenerationRequest) { - if (feGenerationRequest.quizCount() % 5 != 0) { - throw new CustomException(ExceptionMessage.INVALID_QUIZ_COUNT_REQUEST); + private void validateFile(String uploadUrl) { + FileExistStatusResponse fileExistStatusResponse = s3Service.checkFileExistence(uploadUrl); + if (fileExistStatusResponse.status().equals(Status.NOT_EXIST)) { + throw new CustomException(ExceptionMessage.FILE_NOT_FOUND_ON_S3); } + s3ValidateService.checkCloudFrontUrlWithThrowing(uploadUrl); } } \ 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..e7e0ae5a 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; 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') From 2575b3c8c94cf8aa2e9fb0ac6cabd66b20323686 Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:38:55 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[ICC-232]=20Flux=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=9E=91=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icc/qasker/quiz/GenerationService.java | 4 +- .../icc/qasker/quiz/doc/GenerationApiDoc.java | 4 +- .../dto/feResponse/GenerationResponse.java | 11 ---- .../dto/feResponse/ProblemSetResponse.java | 2 + .../qasker/quiz/adapter/AiServerAdapter.java | 2 +- .../quiz/controller/GenerationController.java | 7 ++- .../quiz/mapper/ProblemSetResponseMapper.java | 29 ++++++---- .../quiz/service/GenerationServiceImpl.java | 58 ++++++++++++------- .../quiz/service/ProblemSetServiceImpl.java | 7 +-- 9 files changed, 68 insertions(+), 56 deletions(-) delete mode 100644 modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/GenerationResponse.java 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 3fb8a47d..c4dad5ed 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,12 +1,12 @@ package com.icc.qasker.quiz; import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; -import com.icc.qasker.quiz.dto.feResponse.GenerationResponse; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; import reactor.core.publisher.Flux; public interface GenerationService { - Flux processGenerationRequest(GenerationRequest generationRequest, + Flux processGenerationRequest(GenerationRequest generationRequest, String userId); } 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 index f5ee4eca..1a27293a 100644 --- 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 @@ -1,7 +1,7 @@ package com.icc.qasker.quiz.doc; import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; -import com.icc.qasker.quiz.dto.feResponse.GenerationResponse; +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.web.bind.annotation.PostMapping; @@ -13,7 +13,7 @@ public interface GenerationApiDoc { @Operation(summary = "문제를 생성한다") @PostMapping - Flux postProblemSetId( + Flux postProblemSetId( String userId, @RequestBody GenerationRequest generationRequest); } diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/GenerationResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/GenerationResponse.java deleted file mode 100644 index 41ad0a4a..00000000 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/GenerationResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.icc.qasker.quiz.dto.feResponse; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class GenerationResponse { - - private String problemSetId; -} diff --git a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java index 8f629dc3..c9eea7a7 100644 --- a/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java +++ b/modules/quiz/api/src/main/java/com/icc/qasker/quiz/dto/feResponse/ProblemSetResponse.java @@ -10,6 +10,8 @@ @NoArgsConstructor public class ProblemSetResponse { + private String problemSetId; + private int totalCount; private List quiz; 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 index 3c60b6d8..fb915d52 100644 --- 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 @@ -62,7 +62,7 @@ public Flux requestGenerate(GenerationRequest generati ) ) .bodyToFlux(GenerationResponseFromAI.class) - .timeout(Duration.ofSeconds(30)) + .timeout(Duration.ofSeconds(60)) .doOnError(e -> { if (!(e instanceof ClientSideException) && !(e instanceof CustomException)) { log.error("AI Server Network/Timeout Error: {}", e.getMessage()); 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 ea91b64f..341a7c33 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 @@ -4,7 +4,7 @@ import com.icc.qasker.quiz.GenerationService; import com.icc.qasker.quiz.doc.GenerationApiDoc; import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; -import com.icc.qasker.quiz.dto.feResponse.GenerationResponse; +import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -23,10 +23,11 @@ public class GenerationController implements GenerationApiDoc { @PostMapping(produces = MediaType.APPLICATION_NDJSON_VALUE) @Override - public Flux postProblemSetId( + public Flux postProblemSetId( @UserId String userId, - @Valid @RequestBody GenerationRequest generationRequest) { + @Valid @RequestBody + GenerationRequest generationRequest) { return generationService.processGenerationRequest(generationRequest, userId); } } 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 f4cae7dc..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,5 +1,6 @@ package com.icc.qasker.quiz.mapper; +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; @@ -8,20 +9,16 @@ 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/service/GenerationServiceImpl.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java index 27b2b373..0ff8a6ba 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,57 +1,71 @@ package com.icc.qasker.quiz.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.Status; 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.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.GenerationResponse; +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 com.icc.qasker.util.S3Service; +import com.icc.qasker.util.S3ValidateService; +import com.icc.qasker.util.dto.FileExistStatusResponse; +import com.icc.qasker.util.dto.Status; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; @Service @AllArgsConstructor public class GenerationServiceImpl implements GenerationService { + 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; @Override - public Flux processGenerationRequest( + public Flux processGenerationRequest( GenerationRequest request, String userId) { validateFile(request.uploadedUrl()); ProblemSet problemSet = ProblemSet.builder().userId(userId).build(); - ProblemSet savedPs = problemSetRepository.save(problemSet); - - Flux aiResponse = aiServerAdapter.requestGenerate(request); - - GenerationResponse response = new GenerationResponse( - hashUtil.encode(savedPs.getId()) - ); - - slackNotifier.notifyText(""" - ✅ [퀴즈 생성 완료 알림] - ProblemSet ID: %s - """.formatted( - response.getProblemSetId() - )); + ProblemSet save = problemSetRepository.save(problemSet); + String id = hashUtil.encode(save.getId()); + Scheduler scheduler = Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor()); - return response; + return aiServerAdapter.requestGenerate(request) + .publishOn(scheduler) + .map(aiDto -> { + List quizForFeList = new ArrayList<>(); + for (QuizGeneratedFromAI quizGeneratedFromAI : aiDto.getQuiz()) { + Problem problem = Problem.of(quizGeneratedFromAI, problemSet); + problemRepository.save(problem); + quizForFeList.add(problemSetResponseMapper.fromEntity(problem)); + } + return new ProblemSetResponse( + id, + request.quizCount(), + quizForFeList + ); + }); } private void validateFile(String uploadUrl) { 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 e7e0ae5a..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 @@ -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); - } } From e12a27527b67db4896507229f0d1d0fc25830d0a Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:02:26 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[ICC-232]=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=81=90=20=EA=B8=B0=EB=B0=98webclient=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20=EC=8A=A4=EB=A0=88=EB=93=9C=20=EB=B8=94=EB=A1=9C?= =?UTF-8?q?=ED=82=B9=20=EA=B8=B0=EB=B0=98=20httpclient=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=90=EC=B3=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/quiz/api/build.gradle | 2 +- .../icc/qasker/quiz/GenerationService.java | 5 +- .../icc/qasker/quiz/doc/GenerationApiDoc.java | 7 +- modules/quiz/impl/build.gradle | 9 +- .../qasker/quiz/adapter/AiServerAdapter.java | 86 ----------------- .../AiWebClientConfig.java | 24 ++++- .../quiz/controller/GenerationController.java | 5 +- .../quiz/service/GenerationServiceImpl.java | 94 ++++++++++++------- 8 files changed, 93 insertions(+), 139 deletions(-) delete mode 100644 modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java rename modules/quiz/impl/src/main/java/com/icc/qasker/quiz/{config => adapter}/AiWebClientConfig.java (56%) diff --git a/modules/quiz/api/build.gradle b/modules/quiz/api/build.gradle index 03d063be..473d5f30 100644 --- a/modules/quiz/api/build.gradle +++ b/modules/quiz/api/build.gradle @@ -3,5 +3,5 @@ plugins { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation project(":global") } \ No newline at end of file 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 c4dad5ed..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,12 +1,11 @@ package com.icc.qasker.quiz; import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; -import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; -import reactor.core.publisher.Flux; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; public interface GenerationService { - Flux processGenerationRequest(GenerationRequest generationRequest, + SseEmitter processGenerationRequest(GenerationRequest generationRequest, String userId); } 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 index 1a27293a..c1f12a78 100644 --- 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 @@ -1,19 +1,20 @@ package com.icc.qasker.quiz.doc; +import com.icc.qasker.global.annotation.UserId; import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; -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.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import reactor.core.publisher.Flux; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Tag(name = "Generation", description = "생성 관련 API") public interface GenerationApiDoc { @Operation(summary = "문제를 생성한다") @PostMapping - Flux postProblemSetId( + SseEmitter postProblemSetId( + @UserId String userId, @RequestBody GenerationRequest generationRequest); } diff --git a/modules/quiz/impl/build.gradle b/modules/quiz/impl/build.gradle index f8f1e457..024c6741 100644 --- a/modules/quiz/impl/build.gradle +++ b/modules/quiz/impl/build.gradle @@ -1,13 +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.boot:spring-boot-starter-webflux' + + 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 deleted file mode 100644 index fb915d52..00000000 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiServerAdapter.java +++ /dev/null @@ -1,86 +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.aiResponse.GenerationResponseFromAI; -import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; -import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; -import java.time.Duration; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Slf4j -@Component -public class AiServerAdapter { - - private final WebClient webClient; - - public AiServerAdapter( - @Qualifier("aiStreamClient") WebClient webClient - ) { - this.webClient = webClient; - } - - @CircuitBreaker(name = "aiServer") - public Flux requestGenerate(GenerationRequest generationRequest) { - return webClient.post() - .uri("/generation") - .accept(MediaType.APPLICATION_NDJSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(generationRequest) - .retrieve() - .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> - clientResponse.bodyToMono(String.class) - .flatMap( - errorBody -> { - String message = parseErrorMessage(errorBody); - log.error("[AI Server] 4xx Error: {}", message); - - return Mono.error(new ClientSideException(message)); - } - ) - ) - .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> - clientResponse.bodyToMono(String.class) - .flatMap( - errorBody -> { - String message = parseErrorMessage(errorBody); - log.error("[AI Server] 5xx Error: {}", message); - - return Mono.error(new CustomException( - ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR)); - } - ) - ) - .bodyToFlux(GenerationResponseFromAI.class) - .timeout(Duration.ofSeconds(60)) - .doOnError(e -> { - if (!(e instanceof ClientSideException) && !(e instanceof CustomException)) { - log.error("AI Server Network/Timeout Error: {}", e.getMessage()); - } - }); - } - - - private String parseErrorMessage(String messageBody) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(messageBody); - if (rootNode.has("detail")) { - return rootNode.get("detail").asText(); - } - } catch (Exception ignored) { - - } - return messageBody; - } -} 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/adapter/AiWebClientConfig.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/adapter/AiWebClientConfig.java index ee8d1347..24c9d959 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/adapter/AiWebClientConfig.java @@ -1,16 +1,20 @@ -package com.icc.qasker.quiz.config; +package com.icc.qasker.quiz.adapter; 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; -import org.springframework.web.reactive.function.client.WebClient; @Configuration @AllArgsConstructor @@ -20,9 +24,21 @@ public class AiWebClientConfig { @Primary @Bean("aiStreamClient") - public WebClient aiGenerationClient() { - return WebClient.builder() + 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 빌드 (WebClient와 비슷합니다) + return RestClient.builder() .baseUrl(qAskerProperties.getAiServerUrl()) + .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)) .build(); } 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 341a7c33..2a3c9523 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 @@ -4,7 +4,6 @@ import com.icc.qasker.quiz.GenerationService; import com.icc.qasker.quiz.doc.GenerationApiDoc; import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; -import com.icc.qasker.quiz.dto.feResponse.ProblemSetResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -12,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import reactor.core.publisher.Flux; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @RestController @RequiredArgsConstructor @@ -23,7 +22,7 @@ public class GenerationController implements GenerationApiDoc { @PostMapping(produces = MediaType.APPLICATION_NDJSON_VALUE) @Override - public Flux postProblemSetId( + public SseEmitter postProblemSetId( @UserId String userId, @Valid @RequestBody 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 0ff8a6ba..e02c286d 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,11 +1,13 @@ package com.icc.qasker.quiz.service; +import com.fasterxml.jackson.core.JsonProcessingException; +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.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; @@ -15,18 +17,16 @@ import com.icc.qasker.quiz.mapper.ProblemSetResponseMapper; import com.icc.qasker.quiz.repository.ProblemRepository; import com.icc.qasker.quiz.repository.ProblemSetRepository; -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.Status; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Executors; import lombok.AllArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; +import org.springframework.web.client.RestClient; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Service @AllArgsConstructor @@ -34,45 +34,67 @@ public class GenerationServiceImpl implements GenerationService { 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 RestClient aiStreamClient; + private final ObjectMapper objectMapper; @Override - public Flux processGenerationRequest( + public SseEmitter processGenerationRequest( GenerationRequest request, String userId) { - validateFile(request.uploadedUrl()); + + SseEmitter emitter = new SseEmitter(110 * 1000L); ProblemSet problemSet = ProblemSet.builder().userId(userId).build(); ProblemSet save = problemSetRepository.save(problemSet); String id = hashUtil.encode(save.getId()); - Scheduler scheduler = Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor()); + Thread.ofVirtual().start(() -> { + try { + aiStreamClient.post() + .uri("/generation") + .body(request) + .accept(MediaType.APPLICATION_NDJSON) + .exchange((req, res) -> { + while (true) { + BufferedReader br = new BufferedReader( + new InputStreamReader(res.getBody(), StandardCharsets.UTF_8)); + String line = br.readLine(); + if (line == null) { + break; + } + List quizForFeList = processAIResponse(line, problemSet); + emitter.send( + new ProblemSetResponse( + id, + request.quizCount(), + quizForFeList + )); + } + return null; + }); - return aiServerAdapter.requestGenerate(request) - .publishOn(scheduler) - .map(aiDto -> { - List quizForFeList = new ArrayList<>(); - for (QuizGeneratedFromAI quizGeneratedFromAI : aiDto.getQuiz()) { - Problem problem = Problem.of(quizGeneratedFromAI, problemSet); - problemRepository.save(problem); - quizForFeList.add(problemSetResponseMapper.fromEntity(problem)); - } - return new ProblemSetResponse( - id, - request.quizCount(), - quizForFeList - ); - }); + emitter.complete(); + } catch (Exception e) { + emitter.completeWithError(e); + throw new CustomException(ExceptionMessage.DEFAULT_ERROR); + } + }); + return emitter; } - private void validateFile(String uploadUrl) { - FileExistStatusResponse fileExistStatusResponse = s3Service.checkFileExistence(uploadUrl); - if (fileExistStatusResponse.status().equals(Status.NOT_EXIST)) { - throw new CustomException(ExceptionMessage.FILE_NOT_FOUND_ON_S3); + private List processAIResponse(String line, ProblemSet problemSet) + throws JsonProcessingException { + 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, problemSet); + problems.add(problem); + quizForFeList.add(problemSetResponseMapper.fromEntity(problem)); } - s3ValidateService.checkCloudFrontUrlWithThrowing(uploadUrl); + problemRepository.saveAll(problems); + return quizForFeList; } -} \ No newline at end of file +} From 508ef59a2638ee5a2104f31fb5e64a9b143f580c Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:02:24 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[ICC-232]=20=EA=B0=80=EC=83=81=EC=8A=A4?= =?UTF-8?q?=EB=A0=88=EB=93=9C=EA=B8=B0=EB=B0=98=20=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 1 + .../global/component/SlackNotifier.java | 4 +- .../qasker/quiz/adapter/AIServerAdapter.java | 74 ++++++++++ .../RestClientConfig.java} | 7 +- .../quiz/controller/GenerationController.java | 2 +- .../quiz/repository/ProblemRepository.java | 2 + .../quiz/service/GenerationServiceImpl.java | 133 +++++++++++------- 7 files changed, 168 insertions(+), 55 deletions(-) create mode 100644 modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AIServerAdapter.java rename modules/quiz/impl/src/main/java/com/icc/qasker/quiz/{adapter/AiWebClientConfig.java => config/RestClientConfig.java} (93%) diff --git a/app/build.gradle b/app/build.gradle index 45a4855a..870be73a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,6 +14,7 @@ dependencies { 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/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/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..4d570200 --- /dev/null +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AIServerAdapter.java @@ -0,0 +1,74 @@ +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.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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) -> { + if (res.getStatusCode().isError()) { + String messageBody = new String(res.getBody().readAllBytes(), + StandardCharsets.UTF_8); + if (res.getStatusCode().is4xxClientError()) { + throw new ClientSideException(messageBody); + } + if (res.getStatusCode().is5xxServerError()) { + throw new CustomException( + ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + } + } + while (true) { + BufferedReader br = new BufferedReader(new InputStreamReader(res.getBody(), + StandardCharsets.UTF_8)); + String line = br.readLine(); + if (line == null) { + break; + } + if (line.isBlank()) { + continue; + } + onLineReceived.accept(line); + } + 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); + } + log.error("⚠ AI Server Unknown Error: {}", t.getMessage()); + throw new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR); + } +} diff --git a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/RestClientConfig.java similarity index 93% rename from modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java rename to modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/RestClientConfig.java index 24c9d959..e1e43e26 100644 --- a/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/adapter/AiWebClientConfig.java +++ b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/config/RestClientConfig.java @@ -1,4 +1,4 @@ -package com.icc.qasker.quiz.adapter; +package com.icc.qasker.quiz.config; import com.icc.qasker.global.properties.QAskerProperties; import java.time.Duration; @@ -18,7 +18,7 @@ @Configuration @AllArgsConstructor -public class AiWebClientConfig { +public class RestClientConfig { private final QAskerProperties qAskerProperties; @@ -35,14 +35,13 @@ public RestClient aiGenerationClient(QAskerProperties qAskerProperties) { .setDefaultRequestConfig(requestConfig) .build(); - // 2. RestClient 빌드 (WebClient와 비슷합니다) + // 2. RestClient 빌드 return RestClient.builder() .baseUrl(qAskerProperties.getAiServerUrl()) .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)) .build(); } - @Primary @Bean("aiRestClient") public RestClient aiRestClient() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); 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 2a3c9523..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 @@ -20,7 +20,7 @@ public class GenerationController implements GenerationApiDoc { private final GenerationService generationService; - @PostMapping(produces = MediaType.APPLICATION_NDJSON_VALUE) + @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) @Override public SseEmitter postProblemSetId( @UserId 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/GenerationServiceImpl.java b/modules/quiz/impl/src/main/java/com/icc/qasker/quiz/service/GenerationServiceImpl.java index e02c286d..31b7f96a 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,12 +1,12 @@ package com.icc.qasker.quiz.service; -import com.fasterxml.jackson.core.JsonProcessingException; 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.aiResponse.GenerationResponseFromAI; import com.icc.qasker.quiz.dto.aiResponse.QuizGeneratedFromAI; import com.icc.qasker.quiz.dto.feRequest.GenerationRequest; @@ -17,84 +17,119 @@ import com.icc.qasker.quiz.mapper.ProblemSetResponseMapper; import com.icc.qasker.quiz.repository.ProblemRepository; import com.icc.qasker.quiz.repository.ProblemSetRepository; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; +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 org.springframework.http.MediaType; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; 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 ProblemSetRepository problemSetRepository; private final HashUtil hashUtil; private final ProblemRepository problemRepository; - private final RestClient aiStreamClient; + private final AIServerAdapter aiServerAdapter; private final ObjectMapper objectMapper; @Override public SseEmitter processGenerationRequest( GenerationRequest request, String userId) { - SseEmitter emitter = new SseEmitter(110 * 1000L); - ProblemSet problemSet = ProblemSet.builder().userId(userId).build(); - ProblemSet save = problemSetRepository.save(problemSet); - String id = hashUtil.encode(save.getId()); + CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("aiServer"); + if (circuitBreaker.getState() == State.OPEN) { + sendErrorAndComplete(emitter, + new CustomException(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR)); + return emitter; + } + + 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 finalSaveProblemSet = saveProblemSet; Thread.ofVirtual().start(() -> { try { - aiStreamClient.post() - .uri("/generation") - .body(request) - .accept(MediaType.APPLICATION_NDJSON) - .exchange((req, res) -> { - while (true) { - BufferedReader br = new BufferedReader( - new InputStreamReader(res.getBody(), StandardCharsets.UTF_8)); - String line = br.readLine(); - if (line == null) { - break; - } - List quizForFeList = processAIResponse(line, problemSet); - emitter.send( - new ProblemSetResponse( - id, - request.quizCount(), - quizForFeList - )); - } - return null; - }); - - emitter.complete(); + aiServerAdapter.streamRequest(request, + (line) -> processLine(request, line, emitter, finalSaveProblemSet)); + finalizeSuccess(finalSaveProblemSet.getId(), request, emitter); } catch (Exception e) { - emitter.completeWithError(e); - throw new CustomException(ExceptionMessage.DEFAULT_ERROR); + finalizeError(emitter, e, finalSaveProblemSet); } }); return emitter; } - private List processAIResponse(String line, ProblemSet problemSet) - throws JsonProcessingException { - 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, problemSet); - problems.add(problem); - quizForFeList.add(problemSetResponseMapper.fromEntity(problem)); + private void processLine(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) { + } + } + + private void finalizeSuccess(Long problemSetId, GenerationRequest request, + SseEmitter emitter) { + String encodedId = hashUtil.encode(problemSetId); + int count = problemRepository.countByProblemSetId(problemSetId); + slackNotifier.asyncNotifyText(""" + ✅ [퀴즈 생성 완료 알림] + ProblemSet ID: %s + 퀴즈 타입: %s + 문제 수: %d개 중 %d개 생성됨 + """.formatted( + encodedId, + request.quizType(), + request.quizCount(), + count + )); + emitter.complete(); + } + + private void finalizeError(SseEmitter emitter, Exception e, ProblemSet problemSet) { + sendErrorAndComplete(emitter, e); + // 영속성 컨텍스트 추가 -> 삭제 비용 +// problemSetRepository.delete(problemSet); + problemSetRepository.deleteById(problemSet.getId()); + } + + private void sendErrorAndComplete(SseEmitter emitter, Exception e) { + try { + emitter.send(SseEmitter.event().name("error").data(e.getMessage())); + emitter.complete(); + } catch (IOException ignored) { } - problemRepository.saveAll(problems); - return quizForFeList; } }