Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ public ResponseEntity<ErrorResponse> methodArgumentNotValidException(MethodArgum
public ResponseEntity<ErrorResponse> runtimeException(RuntimeException e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_EXCEPTION));
.body(new ErrorResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Double> 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);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package hongik.triple.commonmodule.dto.analysis;

public record AnalysisRes() {
public record AnalysisRes(
) {
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}