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,15 +1,13 @@
package hongik.triple.apimodule.application.analysis;

import hongik.triple.commonmodule.dto.analysis.AnalysisData;
import hongik.triple.commonmodule.dto.analysis.AnalysisRes;
import hongik.triple.commonmodule.dto.analysis.NaverProductDto;
import hongik.triple.commonmodule.dto.analysis.YoutubeVideoDto;
import hongik.triple.commonmodule.dto.analysis.*;
import hongik.triple.commonmodule.enumerate.AcneType;
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 hongik.triple.inframodule.naver.NaverClient;
import hongik.triple.inframodule.s3.S3Client;
import hongik.triple.inframodule.youtube.YoutubeClient;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand All @@ -29,6 +27,7 @@ public class AnalysisService {
private final YoutubeClient youtubeClient;
private final NaverClient naverClient;
private final AnalysisRepository analysisRepository;
private final S3Client s3Client;

@Transactional
public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
Expand All @@ -38,7 +37,7 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
}

// Business Logic
// TODO: S3 파일 업로드
String s3_key = s3Client.uploadImage(multipartFile, "skin");

// 피부 분석 AI 모델 호출
AnalysisData analysisData = aiClient.sendPredictRequest(multipartFile);
Expand All @@ -55,7 +54,7 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
Analysis analysis = Analysis.builder()
.member(member)
.acneType(analysisData.labelToSkinType())
.imageUrl("S3 URL or other storage URL")
.imageUrl(s3_key)
.isPublic(true)
.videoData(videoList)
.productData(productList)
Expand All @@ -65,7 +64,7 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
// Response
return new AnalysisRes(
saveAnalysis.getAnalysisId(),
saveAnalysis.getImageUrl(),
s3Client.getImage(saveAnalysis.getImageUrl()),
saveAnalysis.getIsPublic(),
AcneType.valueOf(saveAnalysis.getAcneType()).name(),
AcneType.valueOf(saveAnalysis.getAcneType()).getDescription(),
Expand All @@ -88,7 +87,7 @@ public AnalysisRes getAnalysisDetail(Member member, Long analysisId) {
// Response
return new AnalysisRes(
analysis.getAnalysisId(),
analysis.getImageUrl(),
s3Client.getImage(analysis.getImageUrl()),
analysis.getIsPublic(),
AcneType.valueOf(analysis.getAcneType()).name(),
AcneType.valueOf(analysis.getAcneType()).getDescription(),
Expand All @@ -99,14 +98,18 @@ public AnalysisRes getAnalysisDetail(Member member, Long analysisId) {
);
}

public List<AnalysisRes> getAnalysisListForMainPage() {
public MainLogRes getAnalysisListForMainPage() {
// Business Logic
List<Analysis> analyses = analysisRepository.findTop3ByOrderByCreatedAtDesc();
List<Analysis> analyses = analysisRepository.findTop3ByIsPublicTrueOrderByCreatedAtDesc();
int comedones = analysisRepository.countByAcneTypeAndIsPublicTrue("COMEDONES");
int pustules = analysisRepository.countByAcneTypeAndIsPublicTrue("PUSTULES");
int papules = analysisRepository.countByAcneTypeAndIsPublicTrue("PAPULES");
int follicultis = analysisRepository.countByAcneTypeAndIsPublicTrue("FOLLICULITIS");

// Response
return analyses.stream().map(analysis -> new AnalysisRes(
List<AnalysisRes> analysisList = analyses.stream().map(analysis -> new AnalysisRes(
analysis.getAnalysisId(),
analysis.getImageUrl(),
s3Client.getImage(analysis.getImageUrl()),
analysis.getIsPublic(),
AcneType.valueOf(analysis.getAcneType()).name(),
AcneType.valueOf(analysis.getAcneType()).getDescription(),
Expand All @@ -115,6 +118,8 @@ public List<AnalysisRes> getAnalysisListForMainPage() {
analysis.getVideoData(),
analysis.getProductData()
)).toList();

return MainLogRes.from(comedones, pustules, papules, follicultis, analysisList);
}

/**
Expand Down Expand Up @@ -148,7 +153,7 @@ public Page<AnalysisRes> getAnalysisPaginationForLogPage(String acneType, Pageab
// Response
return analysisPage.map(analysis -> new AnalysisRes(
analysis.getAnalysisId(),
analysis.getImageUrl(),
s3Client.getImage(analysis.getImageUrl()),
analysis.getIsPublic(),
AcneType.valueOf(analysis.getAcneType()).name(),
AcneType.valueOf(analysis.getAcneType()).getDescription(),
Expand Down Expand Up @@ -195,7 +200,7 @@ public Page<AnalysisRes> getAnalysisListForMyPage(Member member, String acneType
// Response
return analysisPage.map(analysis -> new AnalysisRes(
analysis.getAnalysisId(),
analysis.getImageUrl(),
s3Client.getImage(analysis.getImageUrl()),
analysis.getIsPublic(),
AcneType.valueOf(analysis.getAcneType()).name(),
AcneType.valueOf(analysis.getAcneType()).getDescription(),
Expand All @@ -205,4 +210,26 @@ public Page<AnalysisRes> getAnalysisListForMyPage(Member member, String acneType
analysis.getProductData()
));
}

/*
피플즈 로그 개별 화면 조회
*/
public AnalysisRes getLogDetail(Long analysisId) {
// Validation
Analysis analysis = analysisRepository.findById(analysisId)
.orElseThrow(() -> new IllegalArgumentException("Analysis not found with id: " + analysisId));

// Response
return new AnalysisRes(
analysis.getAnalysisId(),
s3Client.getImage(analysis.getImageUrl()),
analysis.getIsPublic(),
AcneType.valueOf(analysis.getAcneType()).name(),
AcneType.valueOf(analysis.getAcneType()).getDescription(),
AcneType.valueOf(analysis.getAcneType()).getCareMethod(),
AcneType.valueOf(analysis.getAcneType()).getGuide(),
analysis.getVideoData(),
analysis.getProductData()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import hongik.triple.apimodule.global.security.PrincipalDetails;
import hongik.triple.commonmodule.dto.analysis.AnalysisRes;
import hongik.triple.commonmodule.dto.survey.SurveyRes;
import hongik.triple.inframodule.s3.S3Client;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -25,6 +26,7 @@
public class AnalysisController {

private final AnalysisService analysisService;
private final S3Client s3Client;

@PostMapping("/perform")
@Operation(summary = "피부 이미지 분석", description = "사용자에게 피부 이미지를 전달받아, 분석 결과를 조회합니다.")
Expand All @@ -41,6 +43,7 @@ public ApplicationResponse<?> performAnalysis(@AuthenticationPrincipal Principal
}

@GetMapping("/main")
@Operation(summary = "[홈화면] 피플즈 로그 썸네일 조회", description = "홈화면의 피플즈 로그에 노출되는 상위 3개의 분석 이미지를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "AnceLog Main Page 에서 노출할 피부 분석 이미지 결과 목록",
Expand All @@ -54,20 +57,44 @@ public ApplicationResponse<?> getAnalysisListForMainPage() {
}

@GetMapping("/my")
@Operation(summary = "나의 진단로그 리스트 조회", description = "나의 진단로그 페이지의 리스트를 조회합니다.")
public ApplicationResponse<?> getAnalysisListForMyPage(@AuthenticationPrincipal PrincipalDetails principalDetails,
@RequestParam(name = "type") String acneType,
@PageableDefault(size = 4) Pageable pageable) {
return ApplicationResponse.ok(analysisService.getAnalysisListForMyPage(principalDetails.getMember(), acneType, pageable));
}

@GetMapping("/detail/{analysisId}")
@Operation(summary = "나의 진단로그 상세페이지 조회", description = "나의 진단로그 페이지의 상세 페이지를 조회합니다.")
public ApplicationResponse<?> getAnalysisDetail(@AuthenticationPrincipal PrincipalDetails principalDetails, @PathVariable Long analysisId) {
return ApplicationResponse.ok(analysisService.getAnalysisDetail(principalDetails.getMember(), analysisId));
}

@GetMapping("/log")
@Operation(summary = "피플즈 로그 리스트 조회", description = "피플즈 로그 페이지의 리스트를 조회합니다.")
public ApplicationResponse<?> getAnalysisPaginationForLogPage(@RequestParam(name = "type") String acneType,
@PageableDefault(size = 4) Pageable pageable) {
return ApplicationResponse.ok(analysisService.getAnalysisPaginationForLogPage(acneType, pageable));
}

@GetMapping("/log/{analysisId}")
@Operation(summary = "피플즈 로그 상세페이지 조회", description = "피플즈 로그 페이지의 상세 페이지를 조회합니다.")
public ApplicationResponse<?> getLogDetail(@PathVariable Long analysisId) {
return ApplicationResponse.ok(analysisService.getLogDetail(analysisId));
}

@PostMapping("/image")
@Operation(summary = "이미지 업로드", description = "S3에 이미지를 업로드하는 API 입니다. (어드민용)")
public ApplicationResponse<?> upload(@RequestPart MultipartFile file, @RequestParam(name = "dir") String dir) {

return ApplicationResponse.ok(s3Client.uploadImage(file, dir));
}

@DeleteMapping("/image")
@Operation(summary = "이미지 삭제", description = "S3에서 이미지를 삭제하는 API 입니다. (어드민용)")
public ApplicationResponse<?> delete(@RequestParam String key) {

s3Client.deleteImage(key);
return ApplicationResponse.ok("이미지가 삭제되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package hongik.triple.commonmodule.dto.analysis;

import java.util.List;

public record MainLogRes(
int comedones,
int pustules,
int papules,
int follicultis,
List<AnalysisRes> analysisRes
) {
public static MainLogRes from(int comedones, int pustules, int papules, int follicultis, List<AnalysisRes> analysisRes) {
return new MainLogRes(
comedones,
pustules,
papules,
follicultis,
analysisRes
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ public enum ErrorCode {
ALREADY_DELETE_EXCEPTION(HttpStatus.BAD_REQUEST, 2004, "이미 삭제된 리소스입니다."),
FORBIDDEN_EXCEPTION(HttpStatus.FORBIDDEN, 2005, "인가되지 않는 요청입니다."),
ALREADY_EXIST_EXCEPTION(HttpStatus.BAD_REQUEST, 2006, "이미 존재하는 리소스입니다."),
INVALID_SORT_EXCEPTION(HttpStatus.BAD_REQUEST, 2007, "올바르지 않은 정렬 값입니다.");
INVALID_SORT_EXCEPTION(HttpStatus.BAD_REQUEST, 2007, "올바르지 않은 정렬 값입니다."),

// 3000: Image Error
EMPTY_FILE_EXCEPTION(HttpStatus.BAD_REQUEST, 3000, "파일이 비어있습니다."),
INVALID_FILENAME_EXCEPTION(HttpStatus.BAD_REQUEST, 3001, "파일 이름이 유효하지 않습니다."),
FILE_IO_EXCEPTION(HttpStatus.BAD_REQUEST, 3002, "파일 입출력 처리 중 예상치 못한 오류가 발생했습니다."),
FAILED_UPLOAD_FILE(HttpStatus.INTERNAL_SERVER_ERROR, 3003, "파일 업로드에 실패하였습니다."),
EMPTY_S3_KEY_EXCEPTION(HttpStatus.BAD_REQUEST, 3004, "S3 key 값이 비어있습니다."),
NOT_FOUND_S3_EXCEPTION(HttpStatus.NOT_FOUND, 3005, "존재하지 않는 S3 객체입니다."),
FAILED_DELETE_FILE(HttpStatus.INTERNAL_SERVER_ERROR, 3006, "이미지 삭제에 실패하였습니다."),
NOT_ALLOWED_FILE_EXTENSION(HttpStatus.BAD_REQUEST, 3007, "올바르지 않은 파일 확장자입니다.");

private final HttpStatus httpStatus;
private final Integer code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
public interface AnalysisRepository extends JpaRepository<Analysis, Long> {

// 메인 페이지용
List<Analysis> findTop3ByOrderByCreatedAtDesc();
List<Analysis> findTop3ByIsPublicTrueOrderByCreatedAtDesc();
int countByAcneTypeAndIsPublicTrue(String acneType);

// 피플즈 로그 페이지용 - 전체 공개 분석 조회
Page<Analysis> findByIsPublicTrueOrderByCreatedAtDesc(Pageable pageable);
Expand Down
6 changes: 6 additions & 0 deletions infra-module/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ dependencies {

// WebClient
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// s3
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.766'

// web
implementation 'org.springframework.boot:spring-boot-starter-web'
}

tasks.register("prepareKotlinBuildScriptModel"){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package hongik.triple.inframodule.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {

@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;

@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3 amazonS3() {

BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);

return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}

Loading