From af392bd8c54d5321576b45c4fbfd7f7a9ed76cad Mon Sep 17 00:00:00 2001 From: sseuldev Date: Thu, 20 Nov 2025 02:00:14 +0900 Subject: [PATCH 1/7] =?UTF-8?q?chore(infra-module):=20s3=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra-module/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infra-module/build.gradle b/infra-module/build.gradle index d2e96ae..3dff72f 100644 --- a/infra-module/build.gradle +++ b/infra-module/build.gradle @@ -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"){} \ No newline at end of file From 5c392a443c1cf297109d0500df15e6c6207d4d22 Mon Sep 17 00:00:00 2001 From: sseuldev Date: Thu, 20 Nov 2025 02:02:47 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat(s3):=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C/=EC=A1=B0=ED=9A=8C/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commonmodule/exception/ErrorCode.java | 12 +- .../triple/inframodule/config/S3Config.java | 34 ++++ .../triple/inframodule/s3/S3Client.java | 167 +++++++++++++++++- 3 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 infra-module/src/main/java/hongik/triple/inframodule/config/S3Config.java diff --git a/common-module/src/main/java/hongik/triple/commonmodule/exception/ErrorCode.java b/common-module/src/main/java/hongik/triple/commonmodule/exception/ErrorCode.java index e98d180..5a8bfda 100644 --- a/common-module/src/main/java/hongik/triple/commonmodule/exception/ErrorCode.java +++ b/common-module/src/main/java/hongik/triple/commonmodule/exception/ErrorCode.java @@ -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; diff --git a/infra-module/src/main/java/hongik/triple/inframodule/config/S3Config.java b/infra-module/src/main/java/hongik/triple/inframodule/config/S3Config.java new file mode 100644 index 0000000..0b9d92a --- /dev/null +++ b/infra-module/src/main/java/hongik/triple/inframodule/config/S3Config.java @@ -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(); + } +} + diff --git a/infra-module/src/main/java/hongik/triple/inframodule/s3/S3Client.java b/infra-module/src/main/java/hongik/triple/inframodule/s3/S3Client.java index 037e394..aaeeaec 100644 --- a/infra-module/src/main/java/hongik/triple/inframodule/s3/S3Client.java +++ b/infra-module/src/main/java/hongik/triple/inframodule/s3/S3Client.java @@ -1,18 +1,171 @@ package hongik.triple.inframodule.s3; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import hongik.triple.commonmodule.exception.ApplicationException; +import hongik.triple.commonmodule.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j @Component public class S3Client { + @Value("${cloud.aws.s3.bucket}") + private String bucket; + @Value("${cloud.aws.baseUrl}") + private String baseUrl; + + private final AmazonS3 amazonS3; + + /** + * 이미지 업로드 + */ + public String uploadImage(MultipartFile file, String dirName) { + validateFile(file); + validateImageExtension(file); + + String key = generateFileKey(file.getOriginalFilename(), dirName); + ObjectMetadata metadata = createMetadata(file); + uploadToS3(file, key, metadata); + + return key; + } + /** - * S3 Object에 대한 presigned URL을 생성 - * - * @param fileName 파일 이름 - * @param isUpload true면 업로드를 위한 presigned URL, false면 다운로드를 위한 presigned URL - * @return the presigned URL as a String + * 이미지 조회 + */ + public String getImage(String key) { + validateKey(key); + validateObjectExists(key); + + return baseUrl + "/" + key; + } + + /** + * 이미지 삭제 + */ + public void deleteImage(String key) { + validateKey(key); + validateObjectExists(key); + + deleteObjectFromS3(key); + } + + /* + file 유효성 검사 + */ + private void validateFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new ApplicationException(ErrorCode.EMPTY_FILE_EXCEPTION); + } + if (file.getOriginalFilename() == null || file.getOriginalFilename().isBlank()) { + throw new ApplicationException(ErrorCode.INVALID_FILENAME_EXCEPTION); + } + } + + /* + file 확장자 검사 + */ + private void validateImageExtension(MultipartFile file) { + String originalName = file.getOriginalFilename(); + String extension = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase(); + + // 허용 확장자 목록 + List allowed = List.of("jpg", "jpeg", "png", "gif", "webp"); + + if (!allowed.contains(extension)) { + throw new ApplicationException(ErrorCode.NOT_ALLOWED_FILE_EXTENSION); + } + } + + /* + key 생성 + */ + private String generateFileKey(String originalName, String dirName) { + String date = LocalDate.now().toString(); + String uuid = UUID.randomUUID().toString(); + + return String.format("%s/%s_%s_%s", dirName, date, uuid, originalName); + } + + /* + Metadata 생성 */ - public String getPresignedUrl(String fileName, Boolean isUpload) { - return null; + private ObjectMetadata createMetadata(MultipartFile file) { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + metadata.setContentLength(file.getSize()); + return metadata; } + + /* + S3 업로드 + */ + private void uploadToS3(MultipartFile file, String key, ObjectMetadata metadata) { + try (InputStream input = file.getInputStream()) { + PutObjectRequest request = new PutObjectRequest(bucket, key, input, metadata); + amazonS3.putObject(request); + + } catch (IOException e) { + throw new ApplicationException(ErrorCode.FILE_IO_EXCEPTION); + + } catch (AmazonServiceException e) { + log.error("AWS Service 에러: {}", e.getErrorMessage()); + throw new ApplicationException(ErrorCode.FAILED_UPLOAD_FILE); + + } catch (SdkClientException e) { + log.error("AWS Client 에러: {}", e.getMessage()); + throw new ApplicationException(ErrorCode.FAILED_UPLOAD_FILE); + } + } + + /* + key 유효성 검사 + */ + private void validateKey(String key) { + if (key == null || key.isBlank()) { + throw new ApplicationException(ErrorCode.EMPTY_S3_KEY_EXCEPTION); + } + } + + /* + S3 객체 유효성 검사 + */ + private void validateObjectExists(String key) { + if (!amazonS3.doesObjectExist(bucket, key)) { + throw new ApplicationException(ErrorCode.NOT_FOUND_S3_EXCEPTION); + } + } + + /* + S3 이미지 객체 삭제 + */ + private void deleteObjectFromS3(String key) { + try { + amazonS3.deleteObject(bucket, key); + } catch (AmazonServiceException e) { + log.error("이미지 삭제 중 AWS Service 에러 발생: {}", e.getErrorMessage()); + throw new ApplicationException(ErrorCode.FAILED_DELETE_FILE); + } catch (SdkClientException e) { + log.error("이미지 삭제 중 AWS Client 에러 발생: {}", e.getMessage()); + throw new ApplicationException(ErrorCode.FAILED_DELETE_FILE); + } + } + } From dbaabd631b071c318c37be3169404075e666823a Mon Sep 17 00:00:00 2001 From: sseuldev Date: Thu, 20 Nov 2025 02:03:35 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat(analysis):=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=EC=9A=A9=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/analysis/AnalysisController.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 9e665f2..d6f9c03 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 @@ -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; @@ -25,6 +26,7 @@ public class AnalysisController { private final AnalysisService analysisService; + private final S3Client s3Client; @PostMapping("/perform") @Operation(summary = "피부 이미지 분석", description = "사용자에게 피부 이미지를 전달받아, 분석 결과를 조회합니다.") @@ -70,4 +72,12 @@ public ApplicationResponse getAnalysisPaginationForLogPage(@RequestParam(name @PageableDefault(size = 4) Pageable pageable) { return ApplicationResponse.ok(analysisService.getAnalysisPaginationForLogPage(acneType, pageable)); } + + @DeleteMapping("/image") + @Operation(summary = "이미지 삭제", description = "S3에서 이미지를 삭제하는 API 입니다. (어드민용)") + public ApplicationResponse delete(@RequestParam String key) { + + s3Client.deleteImage(key); + return ApplicationResponse.ok("이미지가 삭제되었습니다."); + } } From e2c41eeff09eafd99272bbc6771b3ceb75bd6e7d Mon Sep 17 00:00:00 2001 From: sseuldev Date: Thu, 20 Nov 2025 02:49:16 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat(analysis):=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=EC=9A=A9=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20API=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/analysis/AnalysisController.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 d6f9c03..a10d515 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 @@ -73,6 +73,13 @@ public ApplicationResponse getAnalysisPaginationForLogPage(@RequestParam(name return ApplicationResponse.ok(analysisService.getAnalysisPaginationForLogPage(acneType, pageable)); } + @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) { From 6c6cf4ea8eb8560089656d7e1d27bec8b4e50458 Mon Sep 17 00:00:00 2001 From: sseuldev Date: Thu, 20 Nov 2025 02:50:46 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat(analysis):=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=A0=84=EC=B2=B4=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/analysis/AnalysisService.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 425acc4..6f7bcd9 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 @@ -10,6 +10,7 @@ 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; @@ -29,6 +30,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) { @@ -38,7 +40,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); @@ -55,7 +57,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) @@ -65,7 +67,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(), @@ -88,7 +90,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(), @@ -106,7 +108,7 @@ public List getAnalysisListForMainPage() { // Response return 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(), @@ -148,7 +150,7 @@ public Page 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(), @@ -195,7 +197,7 @@ public Page 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(), From 1eecd888dca970addc49da992bbf136a9e2af2a2 Mon Sep 17 00:00:00 2001 From: sseuldev Date: Thu, 20 Nov 2025 03:37:41 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat(analysis):=20=ED=94=BC=ED=94=8C?= =?UTF-8?q?=EC=A6=88=20=EB=A1=9C=EA=B7=B8=20=EA=B0=9C=EB=B3=84=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A1=B0=ED=9A=8C=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 | 39 +++++++++++++++---- .../analysis/AnalysisController.java | 10 +++++ 2 files changed, 42 insertions(+), 7 deletions(-) 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 6f7bcd9..7c85f09 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,9 +1,6 @@ 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; @@ -101,12 +98,16 @@ public AnalysisRes getAnalysisDetail(Member member, Long analysisId) { ); } - public List getAnalysisListForMainPage() { + public MainLogRes getAnalysisListForMainPage() { // Business Logic - List analyses = analysisRepository.findTop3ByOrderByCreatedAtDesc(); + List 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 analysisList = analyses.stream().map(analysis -> new AnalysisRes( analysis.getAnalysisId(), s3Client.getImage(analysis.getImageUrl()), analysis.getIsPublic(), @@ -117,6 +118,8 @@ public List getAnalysisListForMainPage() { analysis.getVideoData(), analysis.getProductData() )).toList(); + + return MainLogRes.from(comedones, pustules, papules, follicultis, analysisList); } /** @@ -207,4 +210,26 @@ public Page 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() + ); + } } 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 a10d515..4d729ff 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 @@ -43,6 +43,7 @@ public ApplicationResponse performAnalysis(@AuthenticationPrincipal Principal } @GetMapping("/main") + @Operation(summary = "[홈화면] 피플즈 로그 썸네일 조회", description = "홈화면의 피플즈 로그에 노출되는 상위 3개의 분석 이미지를 조회합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "AnceLog Main Page 에서 노출할 피부 분석 이미지 결과 목록", @@ -56,6 +57,7 @@ public ApplicationResponse getAnalysisListForMainPage() { } @GetMapping("/my") + @Operation(summary = "나의 진단로그 리스트 조회", description = "나의 진단로그 페이지의 리스트를 조회합니다.") public ApplicationResponse getAnalysisListForMyPage(@AuthenticationPrincipal PrincipalDetails principalDetails, @RequestParam(name = "type") String acneType, @PageableDefault(size = 4) Pageable pageable) { @@ -63,16 +65,24 @@ public ApplicationResponse getAnalysisListForMyPage(@AuthenticationPrincipal } @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) { From 7ebe7532182ed0bb173703dd0601d12c73064e78 Mon Sep 17 00:00:00 2001 From: sseuldev Date: Thu, 20 Nov 2025 03:38:47 +0900 Subject: [PATCH 7/7] =?UTF-8?q?hotfix(analysis):=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=94=BC=ED=94=8C=EC=A6=88=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EA=B0=9C=EC=88=98=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commonmodule/dto/analysis/MainLogRes.java | 21 +++++++++++++++++++ .../repository/AnalysisRepository.java | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/MainLogRes.java diff --git a/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/MainLogRes.java b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/MainLogRes.java new file mode 100644 index 0000000..ac40632 --- /dev/null +++ b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/MainLogRes.java @@ -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 +) { + public static MainLogRes from(int comedones, int pustules, int papules, int follicultis, List analysisRes) { + return new MainLogRes( + comedones, + pustules, + papules, + follicultis, + analysisRes + ); + } +} diff --git a/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/repository/AnalysisRepository.java b/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/repository/AnalysisRepository.java index e2445a5..7247cec 100644 --- a/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/repository/AnalysisRepository.java +++ b/domain-module/src/main/java/hongik/triple/domainmodule/domain/analysis/repository/AnalysisRepository.java @@ -11,7 +11,8 @@ public interface AnalysisRepository extends JpaRepository { // 메인 페이지용 - List findTop3ByOrderByCreatedAtDesc(); + List findTop3ByIsPublicTrueOrderByCreatedAtDesc(); + int countByAcneTypeAndIsPublicTrue(String acneType); // 피플즈 로그 페이지용 - 전체 공개 분석 조회 Page findByIsPublicTrueOrderByCreatedAtDesc(Pageable pageable);