Skip to content

Commit d49d558

Browse files
authored
Merge pull request #8 from AcneLog/feat/s3
feat: S3 이미지 로직 개발 및 반영
2 parents 8a55b0f + 7ebe753 commit d49d558

8 files changed

Lines changed: 302 additions & 23 deletions

File tree

api-module/src/main/java/hongik/triple/apimodule/application/analysis/AnalysisService.java

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package hongik.triple.apimodule.application.analysis;
22

3-
import hongik.triple.commonmodule.dto.analysis.AnalysisData;
4-
import hongik.triple.commonmodule.dto.analysis.AnalysisRes;
5-
import hongik.triple.commonmodule.dto.analysis.NaverProductDto;
6-
import hongik.triple.commonmodule.dto.analysis.YoutubeVideoDto;
3+
import hongik.triple.commonmodule.dto.analysis.*;
74
import hongik.triple.commonmodule.enumerate.AcneType;
85
import hongik.triple.domainmodule.domain.analysis.Analysis;
96
import hongik.triple.domainmodule.domain.analysis.repository.AnalysisRepository;
107
import hongik.triple.domainmodule.domain.member.Member;
118
import hongik.triple.inframodule.ai.AIClient;
129
import hongik.triple.inframodule.naver.NaverClient;
10+
import hongik.triple.inframodule.s3.S3Client;
1311
import hongik.triple.inframodule.youtube.YoutubeClient;
1412
import lombok.RequiredArgsConstructor;
1513
import org.springframework.data.domain.Page;
@@ -29,6 +27,7 @@ public class AnalysisService {
2927
private final YoutubeClient youtubeClient;
3028
private final NaverClient naverClient;
3129
private final AnalysisRepository analysisRepository;
30+
private final S3Client s3Client;
3231

3332
@Transactional
3433
public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
@@ -38,7 +37,7 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
3837
}
3938

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

4342
// 피부 분석 AI 모델 호출
4443
AnalysisData analysisData = aiClient.sendPredictRequest(multipartFile);
@@ -55,7 +54,7 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
5554
Analysis analysis = Analysis.builder()
5655
.member(member)
5756
.acneType(analysisData.labelToSkinType())
58-
.imageUrl("S3 URL or other storage URL")
57+
.imageUrl(s3_key)
5958
.isPublic(true)
6059
.videoData(videoList)
6160
.productData(productList)
@@ -65,7 +64,7 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) {
6564
// Response
6665
return new AnalysisRes(
6766
saveAnalysis.getAnalysisId(),
68-
saveAnalysis.getImageUrl(),
67+
s3Client.getImage(saveAnalysis.getImageUrl()),
6968
saveAnalysis.getIsPublic(),
7069
AcneType.valueOf(saveAnalysis.getAcneType()).name(),
7170
AcneType.valueOf(saveAnalysis.getAcneType()).getDescription(),
@@ -88,7 +87,7 @@ public AnalysisRes getAnalysisDetail(Member member, Long analysisId) {
8887
// Response
8988
return new AnalysisRes(
9089
analysis.getAnalysisId(),
91-
analysis.getImageUrl(),
90+
s3Client.getImage(analysis.getImageUrl()),
9291
analysis.getIsPublic(),
9392
AcneType.valueOf(analysis.getAcneType()).name(),
9493
AcneType.valueOf(analysis.getAcneType()).getDescription(),
@@ -99,14 +98,18 @@ public AnalysisRes getAnalysisDetail(Member member, Long analysisId) {
9998
);
10099
}
101100

102-
public List<AnalysisRes> getAnalysisListForMainPage() {
101+
public MainLogRes getAnalysisListForMainPage() {
103102
// Business Logic
104-
List<Analysis> analyses = analysisRepository.findTop3ByOrderByCreatedAtDesc();
103+
List<Analysis> analyses = analysisRepository.findTop3ByIsPublicTrueOrderByCreatedAtDesc();
104+
int comedones = analysisRepository.countByAcneTypeAndIsPublicTrue("COMEDONES");
105+
int pustules = analysisRepository.countByAcneTypeAndIsPublicTrue("PUSTULES");
106+
int papules = analysisRepository.countByAcneTypeAndIsPublicTrue("PAPULES");
107+
int follicultis = analysisRepository.countByAcneTypeAndIsPublicTrue("FOLLICULITIS");
105108

106109
// Response
107-
return analyses.stream().map(analysis -> new AnalysisRes(
110+
List<AnalysisRes> analysisList = analyses.stream().map(analysis -> new AnalysisRes(
108111
analysis.getAnalysisId(),
109-
analysis.getImageUrl(),
112+
s3Client.getImage(analysis.getImageUrl()),
110113
analysis.getIsPublic(),
111114
AcneType.valueOf(analysis.getAcneType()).name(),
112115
AcneType.valueOf(analysis.getAcneType()).getDescription(),
@@ -115,6 +118,8 @@ public List<AnalysisRes> getAnalysisListForMainPage() {
115118
analysis.getVideoData(),
116119
analysis.getProductData()
117120
)).toList();
121+
122+
return MainLogRes.from(comedones, pustules, papules, follicultis, analysisList);
118123
}
119124

120125
/**
@@ -148,7 +153,7 @@ public Page<AnalysisRes> getAnalysisPaginationForLogPage(String acneType, Pageab
148153
// Response
149154
return analysisPage.map(analysis -> new AnalysisRes(
150155
analysis.getAnalysisId(),
151-
analysis.getImageUrl(),
156+
s3Client.getImage(analysis.getImageUrl()),
152157
analysis.getIsPublic(),
153158
AcneType.valueOf(analysis.getAcneType()).name(),
154159
AcneType.valueOf(analysis.getAcneType()).getDescription(),
@@ -195,7 +200,7 @@ public Page<AnalysisRes> getAnalysisListForMyPage(Member member, String acneType
195200
// Response
196201
return analysisPage.map(analysis -> new AnalysisRes(
197202
analysis.getAnalysisId(),
198-
analysis.getImageUrl(),
203+
s3Client.getImage(analysis.getImageUrl()),
199204
analysis.getIsPublic(),
200205
AcneType.valueOf(analysis.getAcneType()).name(),
201206
AcneType.valueOf(analysis.getAcneType()).getDescription(),
@@ -205,4 +210,26 @@ public Page<AnalysisRes> getAnalysisListForMyPage(Member member, String acneType
205210
analysis.getProductData()
206211
));
207212
}
213+
214+
/*
215+
피플즈 로그 개별 화면 조회
216+
*/
217+
public AnalysisRes getLogDetail(Long analysisId) {
218+
// Validation
219+
Analysis analysis = analysisRepository.findById(analysisId)
220+
.orElseThrow(() -> new IllegalArgumentException("Analysis not found with id: " + analysisId));
221+
222+
// Response
223+
return new AnalysisRes(
224+
analysis.getAnalysisId(),
225+
s3Client.getImage(analysis.getImageUrl()),
226+
analysis.getIsPublic(),
227+
AcneType.valueOf(analysis.getAcneType()).name(),
228+
AcneType.valueOf(analysis.getAcneType()).getDescription(),
229+
AcneType.valueOf(analysis.getAcneType()).getCareMethod(),
230+
AcneType.valueOf(analysis.getAcneType()).getGuide(),
231+
analysis.getVideoData(),
232+
analysis.getProductData()
233+
);
234+
}
208235
}

api-module/src/main/java/hongik/triple/apimodule/presentation/analysis/AnalysisController.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import hongik.triple.apimodule.global.security.PrincipalDetails;
66
import hongik.triple.commonmodule.dto.analysis.AnalysisRes;
77
import hongik.triple.commonmodule.dto.survey.SurveyRes;
8+
import hongik.triple.inframodule.s3.S3Client;
89
import io.swagger.v3.oas.annotations.Operation;
910
import io.swagger.v3.oas.annotations.media.Content;
1011
import io.swagger.v3.oas.annotations.media.Schema;
@@ -25,6 +26,7 @@
2526
public class AnalysisController {
2627

2728
private final AnalysisService analysisService;
29+
private final S3Client s3Client;
2830

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

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

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

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

6873
@GetMapping("/log")
74+
@Operation(summary = "피플즈 로그 리스트 조회", description = "피플즈 로그 페이지의 리스트를 조회합니다.")
6975
public ApplicationResponse<?> getAnalysisPaginationForLogPage(@RequestParam(name = "type") String acneType,
7076
@PageableDefault(size = 4) Pageable pageable) {
7177
return ApplicationResponse.ok(analysisService.getAnalysisPaginationForLogPage(acneType, pageable));
7278
}
79+
80+
@GetMapping("/log/{analysisId}")
81+
@Operation(summary = "피플즈 로그 상세페이지 조회", description = "피플즈 로그 페이지의 상세 페이지를 조회합니다.")
82+
public ApplicationResponse<?> getLogDetail(@PathVariable Long analysisId) {
83+
return ApplicationResponse.ok(analysisService.getLogDetail(analysisId));
84+
}
85+
86+
@PostMapping("/image")
87+
@Operation(summary = "이미지 업로드", description = "S3에 이미지를 업로드하는 API 입니다. (어드민용)")
88+
public ApplicationResponse<?> upload(@RequestPart MultipartFile file, @RequestParam(name = "dir") String dir) {
89+
90+
return ApplicationResponse.ok(s3Client.uploadImage(file, dir));
91+
}
92+
93+
@DeleteMapping("/image")
94+
@Operation(summary = "이미지 삭제", description = "S3에서 이미지를 삭제하는 API 입니다. (어드민용)")
95+
public ApplicationResponse<?> delete(@RequestParam String key) {
96+
97+
s3Client.deleteImage(key);
98+
return ApplicationResponse.ok("이미지가 삭제되었습니다.");
99+
}
73100
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package hongik.triple.commonmodule.dto.analysis;
2+
3+
import java.util.List;
4+
5+
public record MainLogRes(
6+
int comedones,
7+
int pustules,
8+
int papules,
9+
int follicultis,
10+
List<AnalysisRes> analysisRes
11+
) {
12+
public static MainLogRes from(int comedones, int pustules, int papules, int follicultis, List<AnalysisRes> analysisRes) {
13+
return new MainLogRes(
14+
comedones,
15+
pustules,
16+
papules,
17+
follicultis,
18+
analysisRes
19+
);
20+
}
21+
}

common-module/src/main/java/hongik/triple/commonmodule/exception/ErrorCode.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,17 @@ public enum ErrorCode {
2020
ALREADY_DELETE_EXCEPTION(HttpStatus.BAD_REQUEST, 2004, "이미 삭제된 리소스입니다."),
2121
FORBIDDEN_EXCEPTION(HttpStatus.FORBIDDEN, 2005, "인가되지 않는 요청입니다."),
2222
ALREADY_EXIST_EXCEPTION(HttpStatus.BAD_REQUEST, 2006, "이미 존재하는 리소스입니다."),
23-
INVALID_SORT_EXCEPTION(HttpStatus.BAD_REQUEST, 2007, "올바르지 않은 정렬 값입니다.");
23+
INVALID_SORT_EXCEPTION(HttpStatus.BAD_REQUEST, 2007, "올바르지 않은 정렬 값입니다."),
24+
25+
// 3000: Image Error
26+
EMPTY_FILE_EXCEPTION(HttpStatus.BAD_REQUEST, 3000, "파일이 비어있습니다."),
27+
INVALID_FILENAME_EXCEPTION(HttpStatus.BAD_REQUEST, 3001, "파일 이름이 유효하지 않습니다."),
28+
FILE_IO_EXCEPTION(HttpStatus.BAD_REQUEST, 3002, "파일 입출력 처리 중 예상치 못한 오류가 발생했습니다."),
29+
FAILED_UPLOAD_FILE(HttpStatus.INTERNAL_SERVER_ERROR, 3003, "파일 업로드에 실패하였습니다."),
30+
EMPTY_S3_KEY_EXCEPTION(HttpStatus.BAD_REQUEST, 3004, "S3 key 값이 비어있습니다."),
31+
NOT_FOUND_S3_EXCEPTION(HttpStatus.NOT_FOUND, 3005, "존재하지 않는 S3 객체입니다."),
32+
FAILED_DELETE_FILE(HttpStatus.INTERNAL_SERVER_ERROR, 3006, "이미지 삭제에 실패하였습니다."),
33+
NOT_ALLOWED_FILE_EXTENSION(HttpStatus.BAD_REQUEST, 3007, "올바르지 않은 파일 확장자입니다.");
2434

2535
private final HttpStatus httpStatus;
2636
private final Integer code;

domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/repository/AnalysisRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
public interface AnalysisRepository extends JpaRepository<Analysis, Long> {
1212

1313
// 메인 페이지용
14-
List<Analysis> findTop3ByOrderByCreatedAtDesc();
14+
List<Analysis> findTop3ByIsPublicTrueOrderByCreatedAtDesc();
15+
int countByAcneTypeAndIsPublicTrue(String acneType);
1516

1617
// 피플즈 로그 페이지용 - 전체 공개 분석 조회
1718
Page<Analysis> findByIsPublicTrueOrderByCreatedAtDesc(Pageable pageable);

infra-module/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ dependencies {
1010

1111
// WebClient
1212
implementation 'org.springframework.boot:spring-boot-starter-webflux'
13+
14+
// s3
15+
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.766'
16+
17+
// web
18+
implementation 'org.springframework.boot:spring-boot-starter-web'
1319
}
1420

1521
tasks.register("prepareKotlinBuildScriptModel"){}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package hongik.triple.inframodule.config;
2+
3+
import com.amazonaws.auth.AWSStaticCredentialsProvider;
4+
import com.amazonaws.auth.BasicAWSCredentials;
5+
import com.amazonaws.services.s3.AmazonS3;
6+
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
@Configuration
12+
public class S3Config {
13+
14+
@Value("${cloud.aws.credentials.access-key}")
15+
private String accessKey;
16+
17+
@Value("${cloud.aws.credentials.secret-key}")
18+
private String secretKey;
19+
20+
@Value("${cloud.aws.region.static}")
21+
private String region;
22+
23+
@Bean
24+
public AmazonS3 amazonS3() {
25+
26+
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
27+
28+
return AmazonS3ClientBuilder.standard()
29+
.withRegion(region)
30+
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
31+
.build();
32+
}
33+
}
34+

0 commit comments

Comments
 (0)