From bbc6ca244370e40be05eabdb3a6cce7896cd0412 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Sun, 28 Sep 2025 21:55:17 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20RuntimeException=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20=EC=8B=9C,=20=EC=97=90=EB=9F=AC=20=EB=94=94?= =?UTF-8?q?=ED=85=8C=EC=9D=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../triple/apimodule/global/common/ErrorResponse.java | 9 +++++---- .../global/exception/GlobalExceptionHandler.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api-module/src/main/java/hongik/triple/apimodule/global/common/ErrorResponse.java b/api-module/src/main/java/hongik/triple/apimodule/global/common/ErrorResponse.java index e826892..ed97e8f 100644 --- a/api-module/src/main/java/hongik/triple/apimodule/global/common/ErrorResponse.java +++ b/api-module/src/main/java/hongik/triple/apimodule/global/common/ErrorResponse.java @@ -7,17 +7,18 @@ public record ErrorResponse( LocalDateTime timestamp, Integer code, - String message) { + String message, + String detail) { public ErrorResponse(ErrorCode errorcode) { - this(LocalDateTime.now(), errorcode.getCode(), errorcode.getMessage()); + this(LocalDateTime.now(), errorcode.getCode(), errorcode.getMessage(), ""); } public ErrorResponse(String message) { - this(LocalDateTime.now(), ErrorCode.INTERNAL_SERVER_EXCEPTION.getCode(), message); + this(LocalDateTime.now(), ErrorCode.INTERNAL_SERVER_EXCEPTION.getCode(), ErrorCode.INTERNAL_SERVER_EXCEPTION.getMessage(), message); } public ErrorResponse(ErrorCode errorcode, String message) { - this(LocalDateTime.now(), errorcode.getCode(), message); + this(LocalDateTime.now(), errorcode.getCode(), errorcode.getMessage(), message); } } diff --git a/api-module/src/main/java/hongik/triple/apimodule/global/exception/GlobalExceptionHandler.java b/api-module/src/main/java/hongik/triple/apimodule/global/exception/GlobalExceptionHandler.java index 3ea72d0..c757a8f 100644 --- a/api-module/src/main/java/hongik/triple/apimodule/global/exception/GlobalExceptionHandler.java +++ b/api-module/src/main/java/hongik/triple/apimodule/global/exception/GlobalExceptionHandler.java @@ -32,6 +32,6 @@ public ResponseEntity methodArgumentNotValidException(MethodArgum public ResponseEntity runtimeException(RuntimeException e) { return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_EXCEPTION)); + .body(new ErrorResponse(e.getMessage())); } } \ No newline at end of file From f90e5fa854df33736ec3bfde3ac9be6801611584 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Sun, 28 Sep 2025 22:10:09 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20AI=20=EB=AA=A8=EB=8D=B8=EC=9D=84=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=B4=EC=84=9C=20=ED=94=BC=EB=B6=80=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B6=84=EC=84=9D=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8A=94=20API=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/analysis/AnalysisService.java | 21 +++++++++- .../analysis/AnalysisController.java | 38 ++++++++++++++----- .../dto/analysis/AnalysisData.java | 27 +++++++++++++ .../dto/analysis/AnalysisRes.java | 3 +- .../domain/analysis/Analysis.java | 17 +++++++++ .../triple/inframodule/ai/AIClient.java | 38 ++++++++++++------- 6 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisData.java diff --git a/api-module/src/main/java/hongik/triple/apimodule/application/analysis/AnalysisService.java b/api-module/src/main/java/hongik/triple/apimodule/application/analysis/AnalysisService.java index 55f4400..8cd5439 100644 --- a/api-module/src/main/java/hongik/triple/apimodule/application/analysis/AnalysisService.java +++ b/api-module/src/main/java/hongik/triple/apimodule/application/analysis/AnalysisService.java @@ -1,22 +1,39 @@ package hongik.triple.apimodule.application.analysis; -import hongik.triple.commonmodule.dto.analysis.AnalysisReq; +import hongik.triple.commonmodule.dto.analysis.AnalysisData; import hongik.triple.commonmodule.dto.analysis.AnalysisRes; +import hongik.triple.domainmodule.domain.analysis.Analysis; import hongik.triple.domainmodule.domain.analysis.repository.AnalysisRepository; +import hongik.triple.domainmodule.domain.member.Member; +import hongik.triple.inframodule.ai.AIClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class AnalysisService { + private final AIClient aiClient; private final AnalysisRepository analysisRepository; - public AnalysisRes performAnalysis(AnalysisReq request) { + public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { // Validation + if(multipartFile.isEmpty() || multipartFile.getSize() == 0) { + throw new IllegalArgumentException("File is empty"); + } + // Business Logic + AnalysisData analysisData = aiClient.sendPredictRequest(multipartFile); + System.out.println(analysisData); + +// Analysis.builder() +// .member(member) +// .skinType(analysisData.labelToSkinType()) +// .build(); + // Response return new AnalysisRes(); // Replace with actual response data } diff --git a/api-module/src/main/java/hongik/triple/apimodule/presentation/analysis/AnalysisController.java b/api-module/src/main/java/hongik/triple/apimodule/presentation/analysis/AnalysisController.java index a7c5e1d..69c9d0e 100644 --- a/api-module/src/main/java/hongik/triple/apimodule/presentation/analysis/AnalysisController.java +++ b/api-module/src/main/java/hongik/triple/apimodule/presentation/analysis/AnalysisController.java @@ -2,10 +2,20 @@ import hongik.triple.apimodule.application.analysis.AnalysisService; import hongik.triple.apimodule.global.common.ApplicationResponse; +import hongik.triple.apimodule.global.security.PrincipalDetails; import hongik.triple.commonmodule.dto.analysis.AnalysisReq; +import hongik.triple.commonmodule.dto.analysis.AnalysisRes; +import hongik.triple.commonmodule.dto.survey.SurveyRes; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/v1/analysis") @@ -16,18 +26,28 @@ public class AnalysisController { private final AnalysisService analysisService; @PostMapping("/perform") - public ApplicationResponse performAnalysis() { - analysisService.performAnalysis(new AnalysisReq()); - return ApplicationResponse.ok(); - } - - // Polling for analysis results from AI model worker - @GetMapping("/poll") - public ApplicationResponse pollAnalysis() { - return ApplicationResponse.ok(); + @Operation(summary = "피부 이미지 분석", description = "사용자에게 피부 이미지를 전달받아, 분석 결과를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "피부이미지 분석 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = AnalysisRes.class))), + @ApiResponse(responseCode = "500", + description = "서버 오류") + }) + public ApplicationResponse performAnalysis(@RequestPart(value = "file") MultipartFile multipartFile) { + return ApplicationResponse.ok(analysisService.performAnalysis(null, multipartFile)); } @GetMapping("/main") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "AnceLog Main Page 에서 노출할 피부 분석 이미지 결과 목록", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = SurveyRes.class))), + @ApiResponse(responseCode = "500", + description = "서버 오류") + }) public ApplicationResponse getAnalysisListForMainPage() { return ApplicationResponse.ok(); } diff --git a/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisData.java b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisData.java new file mode 100644 index 0000000..8a0242f --- /dev/null +++ b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisData.java @@ -0,0 +1,27 @@ +package hongik.triple.commonmodule.dto.analysis; + +import com.fasterxml.jackson.annotation.JsonProperty; +import hongik.triple.commonmodule.enumerate.SkinType; + +import java.util.List; + +public record AnalysisData( + @JsonProperty("prediction_index") + Integer predictionIndex, + @JsonProperty("prediction_label") + String predictionLabel, + @JsonProperty("prediction_confidence") + Double predictionConfidence, + List scores +) { + + public SkinType labelToSkinType() { + return switch (this.predictionLabel) { + case "Comedones" -> SkinType.COMEDONES; + case "Pustules" -> SkinType.PUSTULES; + case "Papules" -> SkinType.PAPULES; + case "Folliculitis" -> SkinType.FOLLICULITIS; + default -> throw new IllegalArgumentException("Unknown label: " + this.predictionLabel); + }; + } +} diff --git a/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisRes.java b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisRes.java index f91e49a..e8b95cd 100644 --- a/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisRes.java +++ b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/AnalysisRes.java @@ -1,4 +1,5 @@ package hongik.triple.commonmodule.dto.analysis; -public record AnalysisRes() { +public record AnalysisRes( +) { } diff --git a/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/Analysis.java b/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/Analysis.java index cc35b02..980ee35 100644 --- a/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/Analysis.java +++ b/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/Analysis.java @@ -1,7 +1,10 @@ package hongik.triple.domainmodule.domain.analysis; +import hongik.triple.commonmodule.enumerate.SkinType; import hongik.triple.domainmodule.common.BaseTimeEntity; +import hongik.triple.domainmodule.domain.member.Member; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLDelete; @@ -17,4 +20,18 @@ public class Analysis extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "analysis_id") private Long analysisId; + + @JoinColumn(name = "member_id") //, nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @Column(name = "skin_type", nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private SkinType skinType; + + @Builder + public Analysis(Member member, SkinType skinType) { + this.member = member; + this.skinType = skinType; + } } diff --git a/infra-module/src/main/java/hongik/triple/inframodule/ai/AIClient.java b/infra-module/src/main/java/hongik/triple/inframodule/ai/AIClient.java index 860db7e..1cb9e44 100644 --- a/infra-module/src/main/java/hongik/triple/inframodule/ai/AIClient.java +++ b/infra-module/src/main/java/hongik/triple/inframodule/ai/AIClient.java @@ -1,33 +1,45 @@ package hongik.triple.inframodule.ai; -import lombok.RequiredArgsConstructor; +import hongik.triple.commonmodule.dto.analysis.AnalysisData; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.WebClientRequestException; @Component public class AIClient { private final WebClient webClient; - public AIClient(WebClient.Builder webClientBuilder) { + /** + * WebClient를 사용하여 FastAPI 서버와 통신 + * + * @param webClientBuilder WebClient 빌더 + * @param baseUrl FastAPI 서버의 URL (application-infra.yml 에서 주입) + */ + public AIClient(WebClient.Builder webClientBuilder, + @Value("${ai.server-url}") String baseUrl) { + System.out.println("AIClient initialized with baseUrl: " + baseUrl); this.webClient = webClientBuilder - .baseUrl("https://api.example.com") // AI 모델의 기본 URL 설정 + .baseUrl(baseUrl) // 환경설정 값 사용 .build(); } /** - * AI 모델에 요청을 보내고 응답을 받는 메서드 + * FastAPI 서버로 이미지 전송 후 예측 결과 받기 * - * @param requestBody 요청 본문 - * @return AI 모델의 응답 + * @param file 업로드할 이미지 파일 + * @return FastAPI 모델의 JSON 응답 */ - public String sendRequest(String requestBody) { + public AnalysisData sendPredictRequest(MultipartFile file) { return webClient.post() - .uri("/ai-endpoint") // AI 모델의 엔드포인트 URI - .bodyValue(requestBody) + .uri("/predict") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData("file", file.getResource())) .retrieve() - .bodyToMono(String.class) - .block(); // 블로킹 방식으로 응답을 기다림 + .bodyToMono(AnalysisData.class) + .block(); } -} +} \ No newline at end of file