From bde776296648f9721cdd4826bc4ab4776c983914 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Thu, 2 Oct 2025 17:32:17 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20AcneType=EA=B3=BC=20SkinType=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/survey/SurveyService.java | 28 ++++++++----------- .../commonmodule/dto/survey/SurveyRes.java | 1 + .../commonmodule/enumerate/SkinType.java | 10 +++---- .../domainmodule/domain/member/Member.java | 3 +- .../domainmodule/domain/survey/Survey.java | 6 ++-- .../survey/repository/SurveyRepository.java | 2 +- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/api-module/src/main/java/hongik/triple/apimodule/application/survey/SurveyService.java b/api-module/src/main/java/hongik/triple/apimodule/application/survey/SurveyService.java index 1ab168a..153febe 100644 --- a/api-module/src/main/java/hongik/triple/apimodule/application/survey/SurveyService.java +++ b/api-module/src/main/java/hongik/triple/apimodule/application/survey/SurveyService.java @@ -53,7 +53,7 @@ public SurveyRes registerSurvey(SurveyReq request) { .surveyId(savedSurvey.getSurveyId()) .memberId(savedSurvey.getMember().getMemberId()) .memberName(savedSurvey.getMember().getName()) - .skinType(savedSurvey.getSkinType()) + .skinType(SkinType.valueOf(savedSurvey.getSkinType())) .body((Map) savedSurvey.getBody()) .createdAt(savedSurvey.getCreatedAt()) .modifiedAt(savedSurvey.getModifiedAt()) @@ -146,17 +146,17 @@ private SkinType calculateSkinType(Map answers) { // 점수 기반 피부 타입 결정 (심각도 순으로 우선순위) if (averageScore <= 2.0) { - return SkinType.NORMAL; + return SkinType.OILY; } else if (folliculitisScore >= 8) { // 모낭염 문항 평균 4점 이상 - return SkinType.FOLLICULITIS; + return SkinType.OILY; } else if (pustuleScore >= 8) { // 화농성 문항 평균 4점 이상 - return SkinType.PUSTULES; + return SkinType.OILY; } else if (inflammationScore >= 15) { // 염증성 문항 평균 3.75점 이상 - return SkinType.PAPULES; + return SkinType.OILY; } else if (comedoneScore >= 12) { // 좁쌀 문항 평균 3점 이상 - return SkinType.COMEDONES; + return SkinType.OILY; } else { - return SkinType.NORMAL; + return SkinType.OILY; } } @@ -273,16 +273,12 @@ private int calculateTotalScore(Map body) { private String generateRecommendation(SkinType skinType) { switch (skinType) { - case NORMAL: + case OILY: return "현재 피부 상태가 양호합니다. 기본적인 세안과 보습 관리를 지속하시고, 자외선 차단제를 꾸준히 사용하세요."; - case COMEDONES: + case COMBINATION: return "좁쌀여드름이 있습니다. BHA나 살리실산 성분의 각질 제거 제품을 사용하고, 논코메도제닉 제품으로 모공 관리에 집중하세요."; - case PUSTULES: + case DRY: return "화농성 여드름이 있습니다. 벤조일 퍼옥사이드나 항생제 성분이 포함된 제품을 사용하고, 피부과 전문의 상담을 받아보세요."; - case PAPULES: - return "염증성 여드름이 있습니다. 자극적인 제품 사용을 피하고 니아신아마이드, 아젤라산 등의 진정 성분으로 관리하며, 피부과 치료를 권장합니다."; - case FOLLICULITIS: - return "모낭염이 의심됩니다. 면도 후 항균 토너를 사용하고, 청결한 관리와 함께 피부과 전문의 진료를 받으시기 바랍니다."; default: return "정확한 진단을 위해 피부과 전문의와 상담을 받아보세요."; } @@ -481,12 +477,12 @@ private SurveyRes convertToSurveyRes(Survey survey) { .surveyId(survey.getSurveyId()) .memberId(survey.getMember().getMemberId()) .memberName(survey.getMember().getName()) - .skinType(survey.getSkinType()) + .skinType(SkinType.valueOf(survey.getSkinType())) .body((Map) survey.getBody()) .createdAt(survey.getCreatedAt()) .modifiedAt(survey.getModifiedAt()) .totalScore(calculateTotalScore((Map) survey.getBody())) - .recommendation(generateRecommendation(survey.getSkinType())) + .recommendation(generateRecommendation(SkinType.valueOf(survey.getSkinType()))) .build(); } } diff --git a/common-module/src/main/java/hongik/triple/commonmodule/dto/survey/SurveyRes.java b/common-module/src/main/java/hongik/triple/commonmodule/dto/survey/SurveyRes.java index 9fb724b..41ea634 100644 --- a/common-module/src/main/java/hongik/triple/commonmodule/dto/survey/SurveyRes.java +++ b/common-module/src/main/java/hongik/triple/commonmodule/dto/survey/SurveyRes.java @@ -1,5 +1,6 @@ package hongik.triple.commonmodule.dto.survey; +import hongik.triple.commonmodule.enumerate.AcneType; import hongik.triple.commonmodule.enumerate.SkinType; import lombok.Builder; diff --git a/common-module/src/main/java/hongik/triple/commonmodule/enumerate/SkinType.java b/common-module/src/main/java/hongik/triple/commonmodule/enumerate/SkinType.java index 51a9fa9..047095a 100644 --- a/common-module/src/main/java/hongik/triple/commonmodule/enumerate/SkinType.java +++ b/common-module/src/main/java/hongik/triple/commonmodule/enumerate/SkinType.java @@ -5,15 +5,13 @@ @Getter public enum SkinType { - NORMAL("정상"), - COMEDONES("좁쌀"), - PUSTULES("화농성"), - PAPULES("염증성"), - FOLLICULITIS("모낭염"); + OILY("지성"), + DRY("건성"), + COMBINATION("수부지"); private final String description; SkinType(String description) { this.description = description; } -} +} \ No newline at end of file diff --git a/domain-module/src/main/java/hongik/triple/domainmodule/domain/member/Member.java b/domain-module/src/main/java/hongik/triple/domainmodule/domain/member/Member.java index 1e10e62..8d54ff9 100644 --- a/domain-module/src/main/java/hongik/triple/domainmodule/domain/member/Member.java +++ b/domain-module/src/main/java/hongik/triple/domainmodule/domain/member/Member.java @@ -29,7 +29,7 @@ public class Member extends BaseTimeEntity { private MemberType memberType; @Column(name = "skin_type") - private String skinType; // SkinType enum의 값을 문자열로 저장 + private String skinType; // AcneType enum의 값을 문자열로 저장 public void updateSkinType(String skinType) { this.skinType = skinType; @@ -39,6 +39,5 @@ public Member(String name, String email, MemberType memberType) { this.name = name; this.email = email; this.memberType = memberType; - this.skinType = "normal"; // 기본값 설정 } } diff --git a/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java b/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java index b615512..bafbdc9 100644 --- a/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java +++ b/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java @@ -1,5 +1,6 @@ package hongik.triple.domainmodule.domain.survey; +import hongik.triple.commonmodule.enumerate.AcneType; import hongik.triple.commonmodule.enumerate.SkinType; import hongik.triple.domainmodule.common.BaseTimeEntity; import hongik.triple.domainmodule.domain.member.Member; @@ -31,12 +32,13 @@ public class Survey extends BaseTimeEntity { @Enumerated(EnumType.STRING) @Column(name = "skin_type", nullable = false) - private SkinType skinType; + // @Enumerated(EnumType.STRING) 사용 X, String 형식으로 저장 (이유: description도 같이 저장되는 것을 방지하기 위해) + private String skinType; @Builder public Survey(Member member, Object body, SkinType skinType) { this.member = member; this.body = body; - this.skinType = skinType; + this.skinType = skinType.name(); } } diff --git a/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/repository/SurveyRepository.java b/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/repository/SurveyRepository.java index ac7931f..e842e40 100644 --- a/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/repository/SurveyRepository.java +++ b/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/repository/SurveyRepository.java @@ -17,7 +17,7 @@ public interface SurveyRepository extends JpaRepository { Page findAllByOrderByCreatedAtDesc(Pageable pageable); - List findByMember_MemberIdAndSkinType(Long memberId, SkinType skinType); + List findAllByMember_MemberIdAndSkinType(Long memberId, SkinType skinType); @Query("SELECT s FROM Survey s WHERE s.createdAt >= :startDate AND s.createdAt <= :endDate") List findByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, From baa35aa3075b6cf93ffef5a2e8a83e731e896b52 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Tue, 4 Nov 2025 17:53:41 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20Youtube=20=EC=98=81=EC=83=81=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=B6=80=EB=B6=84=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 ++- .../dto/analysis/AnalysisData.java | 12 +- .../commonmodule/enumerate/AcneType.java | 19 +++ .../domain/analysis/Analysis.java | 20 ++- .../domainmodule/domain/survey/Survey.java | 1 - .../inframodule/youtube/YoutubeClient.java | 161 ++++++++++++++++++ 6 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java create mode 100644 infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.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 8cd5439..dc775fa 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 @@ -6,6 +6,8 @@ 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.youtube.YoutubeClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +19,8 @@ public class AnalysisService { private final AIClient aiClient; + private final YoutubeClient youtubeClient; + private final NaverClient naverClient; private final AnalysisRepository analysisRepository; public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { @@ -26,13 +30,20 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { } // Business Logic + // 피부 분석 AI 모델 호출 AnalysisData analysisData = aiClient.sendPredictRequest(multipartFile); - System.out.println(analysisData); -// Analysis.builder() -// .member(member) -// .skinType(analysisData.labelToSkinType()) -// .build(); + // 진단 결과를 기반으로 피부 관리 영상 추천 (유튜브 API) + var videos = youtubeClient.searchVideos(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); + + // 진단 결과를 기반으로 맞춤형 제품 추천 (네이버 쇼핑 API) +// var products = naverClient.searchProducts(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); + + // DB 저장 + Analysis.builder() + .member(member) + .acneType(analysisData.labelToSkinType()) + .build(); // Response return new AnalysisRes(); // Replace with actual response data 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 index 8a0242f..59c5d12 100644 --- 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 @@ -1,7 +1,7 @@ package hongik.triple.commonmodule.dto.analysis; import com.fasterxml.jackson.annotation.JsonProperty; -import hongik.triple.commonmodule.enumerate.SkinType; +import hongik.triple.commonmodule.enumerate.AcneType; import java.util.List; @@ -15,12 +15,12 @@ public record AnalysisData( List scores ) { - public SkinType labelToSkinType() { + public AcneType labelToSkinType() { return switch (this.predictionLabel) { - case "Comedones" -> SkinType.COMEDONES; - case "Pustules" -> SkinType.PUSTULES; - case "Papules" -> SkinType.PAPULES; - case "Folliculitis" -> SkinType.FOLLICULITIS; + case "Comedones" -> AcneType.COMEDONES; + case "Pustules" -> AcneType.PUSTULES; + case "Papules" -> AcneType.PAPULES; + case "Folliculitis" -> AcneType.FOLLICULITIS; default -> throw new IllegalArgumentException("Unknown label: " + this.predictionLabel); }; } diff --git a/common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java b/common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java new file mode 100644 index 0000000..209dcc4 --- /dev/null +++ b/common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java @@ -0,0 +1,19 @@ +package hongik.triple.commonmodule.enumerate; + +import lombok.Getter; + +@Getter +public enum AcneType { + + NORMAL("정상"), + COMEDONES("좁쌀"), + PUSTULES("화농성"), + PAPULES("염증성"), + FOLLICULITIS("모낭염"); + + private final String description; + + AcneType(String description) { + this.description = description; + } +} 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 980ee35..cc43fa0 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,19 +1,22 @@ package hongik.triple.domainmodule.domain.analysis; -import hongik.triple.commonmodule.enumerate.SkinType; +import hongik.triple.commonmodule.enumerate.AcneType; import hongik.triple.domainmodule.common.BaseTimeEntity; import hongik.triple.domainmodule.domain.member.Member; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.annotations.SQLDelete; +import org.hibernate.type.SqlTypes; @Entity @Getter @Table(name = "analysis") @SQLDelete(sql = "UPDATE analysis SET deleted_at = NOW() where banner_id = ?") -@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Analysis extends BaseTimeEntity { @Id @@ -25,13 +28,16 @@ public class Analysis extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Member member; - @Column(name = "skin_type", nullable = false, length = 20) - @Enumerated(EnumType.STRING) - private SkinType skinType; + @Column(name = "acne_type", nullable = false, length = 20) + private String acneType; // @Enumerated(EnumType.STRING) 사용 X, String 형식으로 저장 + +// @JdbcTypeCode(SqlTypes.JSON) +// @Column(name = "recommend_youtube", columnDefinition = "json") +// private RecommendYoutube recommendYoutube; @Builder - public Analysis(Member member, SkinType skinType) { + public Analysis(Member member, AcneType acneType) { this.member = member; - this.skinType = skinType; + this.acneType = acneType.name(); } } diff --git a/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java b/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java index bafbdc9..17fcdf8 100644 --- a/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java +++ b/domain-module/src/main/java/hongik/triple/domainmodule/domain/survey/Survey.java @@ -30,7 +30,6 @@ public class Survey extends BaseTimeEntity { private Object body; - @Enumerated(EnumType.STRING) @Column(name = "skin_type", nullable = false) // @Enumerated(EnumType.STRING) 사용 X, String 형식으로 저장 (이유: description도 같이 저장되는 것을 방지하기 위해) private String skinType; diff --git a/infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.java b/infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.java new file mode 100644 index 0000000..67efa3f --- /dev/null +++ b/infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.java @@ -0,0 +1,161 @@ +package hongik.triple.inframodule.youtube; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class YoutubeClient { + + private final WebClient webClient; + private final String apiKey; + + public YoutubeClient( + WebClient.Builder webClientBuilder, + @Value("${youtube.api.key}") String apiKey, + @Value("${youtube.api.base-url}") String baseUrl) { + this.webClient = webClientBuilder.baseUrl(baseUrl).build(); + this.apiKey = apiKey; + } + + /** + * 키워드로 YouTube 영상 검색 + */ + public List searchVideos(String query, int maxResults) { + try { + log.info("YouTube 검색 시작 - query: {}, maxResults: {}", query, maxResults); + + YoutubeSearchResponse response = webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/search") + .queryParam("part", "snippet") + .queryParam("q", query) + .queryParam("type", "video") + .queryParam("maxResults", maxResults) + .queryParam("key", apiKey) + // .queryParam("order", "relevance") // 기본값이 relevance라 생략 가능 + // .queryParam("regionCode", "KR") // 선택 사항 + // .queryParam("relevanceLanguage", "ko") // 선택 사항 + .build()) + .retrieve() + .onStatus( + status -> status.is4xxClientError() || status.is5xxServerError(), + clientResponse -> clientResponse.bodyToMono(String.class) + .doOnNext(errorBody -> + log.error("YouTube API 에러 응답: {}", errorBody)) + .then(Mono.error(new RuntimeException("YouTube API 호출 실패"))) + ) + .bodyToMono(YoutubeSearchResponse.class) + .block(); + + if (response == null || response.items == null) { + log.warn("YouTube API 응답이 비어있습니다."); + return List.of(); + } + + log.info("YouTube 검색 성공 - {} 개의 결과 반환", response.items.size()); + + return response.items.stream() + .map(item -> new YoutubeVideoDto( + item.id.videoId, + item.snippet.title, + "https://www.youtube.com/watch?v=" + item.id.videoId, + item.snippet.channelTitle, + item.snippet.thumbnails.high != null + ? item.snippet.thumbnails.high.url + : (item.snippet.thumbnails.defaultThumbnail != null + ? item.snippet.thumbnails.defaultThumbnail.url + : "") + )) + .collect(Collectors.toList()); + + } catch (WebClientResponseException e) { + log.error("YouTube API 호출 실패 - Status: {}, Body: {}", + e.getStatusCode(), e.getResponseBodyAsString()); + return List.of(); + } catch (Exception e) { + log.error("YouTube 검색 중 예외 발생 - query: {}", query, e); + return List.of(); + } + } + + /** + * 진단명 기반 영상 추천 + */ + public List getRecommendationsByDiagnosis(String diagnosisName) { + String query = diagnosisName + " 피부 관리"; // "치료" 제거 (더 넓은 검색) + return searchVideos(query, 5); + } + + /** + * 피부 타입 기반 영상 추천 + */ + public List getRecommendationsBySkinType(String skinType) { + String query = skinType + " 피부 관리"; // "스킨케어" 제거 (더 넓은 검색) + return searchVideos(query, 3); + } + + // Response DTOs + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class YoutubeSearchResponse { + private List items; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class YoutubeSearchItem { + private VideoId id; + private VideoSnippet snippet; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class VideoId { + private String videoId; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class VideoSnippet { + private String title; + private String channelTitle; + private Thumbnails thumbnails; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Thumbnails { + @JsonProperty("high") + private Thumbnail high; + @JsonProperty("default") + private Thumbnail defaultThumbnail; + @JsonProperty("medium") + private Thumbnail medium; // 추가: fallback 옵션 + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Thumbnail { + private String url; + } + + // Result DTO + public record YoutubeVideoDto( + String videoId, + String videoTitle, + String videoUrl, + String channelName, + String thumbnailUrl + ) {} +} \ No newline at end of file From 9332f36ef55357a210b82b8e7ec7f4359e7df760 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Tue, 4 Nov 2025 18:01:00 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20Naver=20=EC=8A=A4=EB=A7=88=ED=8A=B8?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EC=96=B4=20=EC=A0=9C=ED=92=88=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EB=B6=80=EB=B6=84=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 | 2 +- .../triple/inframodule/naver/NaverClient.java | 186 ++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.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 dc775fa..4f66894 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 @@ -37,7 +37,7 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { var videos = youtubeClient.searchVideos(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); // 진단 결과를 기반으로 맞춤형 제품 추천 (네이버 쇼핑 API) -// var products = naverClient.searchProducts(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); + var products = naverClient.searchProducts(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); // DB 저장 Analysis.builder() diff --git a/infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.java b/infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.java new file mode 100644 index 0000000..f7da817 --- /dev/null +++ b/infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.java @@ -0,0 +1,186 @@ +package hongik.triple.inframodule.naver; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class NaverClient { + // Reference: https://developers.naver.com/docs/serviceapi/search/shopping/shopping.md + + private final WebClient webClient; + private final String clientId; + private final String clientSecret; + + private static final String DOMAIN = "https://openapi.naver.com"; + private static final String SEARCH_PATH = "/v1/search/shop.json"; + + public NaverClient( + WebClient.Builder webClientBuilder, + @Value("${naver.api.client-id}") String clientId, + @Value("${naver.api.client-secret}") String clientSecret) { + this.webClient = webClientBuilder.baseUrl(DOMAIN).build(); + this.clientId = clientId; + this.clientSecret = clientSecret; + } + + /** + * 네이버 쇼핑 API - 상품 검색 + */ + public List searchProducts(String keyword, int display) { + try { + NaverShoppingResponse response = webClient.get() + .uri(uriBuilder -> uriBuilder + .path(SEARCH_PATH) + .queryParam("query", keyword) + .queryParam("display", display) + .queryParam("sort", "sim") // sim: 정확도순, date: 날짜순, asc/dsc: 가격 오름차순/내림차순 + .build()) + .header("X-Naver-Client-Id", clientId) + .header("X-Naver-Client-Secret", clientSecret) + .retrieve() + .bodyToMono(NaverShoppingResponse.class) + .block(); + + if (response == null || response.items == null || response.items.isEmpty()) { + log.warn("No products found for keyword: {}", keyword); + return List.of(); + } + + return response.items.stream() + .map(item -> new NaverProductDto( + item.productId, + removeHtmlTags(item.title), + item.link, + item.lprice, + item.image, + item.category1, + item.mallName, + item.brand + )) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("Failed to search Naver shopping products for keyword: {}", keyword, e); + return List.of(); + } + } + + /** + * 진단명 기반 상품 추천 + */ + public List getRecommendationsByDiagnosis(String diagnosisName) { + String keyword = diagnosisName + " 피부 치료 크림"; + return searchProducts(keyword, 5); + } + + /** + * 피부 타입 기반 상품 추천 + */ + public List getRecommendationsBySkinType(String skinType) { + String keyword = skinType + " 피부 스킨케어"; + return searchProducts(keyword, 3); + } + + /** + * 가격대별 상품 검색 + */ + public List searchProductsByPriceRange(String keyword, int minPrice, int maxPrice, int display) { + try { + NaverShoppingResponse response = webClient.get() + .uri(uriBuilder -> uriBuilder + .path(SEARCH_PATH) + .queryParam("query", keyword) + .queryParam("display", display) + .queryParam("sort", "asc") // 가격 오름차순 + .build()) + .header("X-Naver-Client-Id", clientId) + .header("X-Naver-Client-Secret", clientSecret) + .retrieve() + .bodyToMono(NaverShoppingResponse.class) + .block(); + + if (response == null || response.items == null || response.items.isEmpty()) { + return List.of(); + } + + // 가격 필터링 + return response.items.stream() + .filter(item -> item.lprice >= minPrice && item.lprice <= maxPrice) + .map(item -> new NaverProductDto( + item.productId, + removeHtmlTags(item.title), + item.link, + item.lprice, + item.image, + item.category1, + item.mallName, + item.brand + )) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("Failed to search products with price range: {} ~ {}", minPrice, maxPrice, e); + return List.of(); + } + } + + /** + * HTML 태그 제거 (네이버 API는 제목에 태그를 포함하여 반환) + */ + private String removeHtmlTags(String text) { + if (text == null) { + return ""; + } + return text.replaceAll("<[^>]*>", ""); + } + + // Response DTOs + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class NaverShoppingResponse { + private String lastBuildDate; + private int total; + private int start; + private int display; + private List items; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class NaverShoppingItem { + private String title; // 상품명 + private String link; // 상품 URL + private String image; // 상품 이미지 URL + private int lprice; // 최저가 + private int hprice; // 최고가 + private String mallName; // 쇼핑몰 이름 + private String productId; // 상품 ID + private String productType; // 상품 타입 + private String brand; // 브랜드 + private String maker; // 제조사 + private String category1; // 카테고리1 + private String category2; // 카테고리2 + private String category3; // 카테고리3 + private String category4; // 카테고리4 + } + + // Result DTO + public record NaverProductDto( + String productId, + String productName, + String productUrl, + Integer productPrice, + String productImage, + String categoryName, + String mallName, + String brand + ) {} +} \ No newline at end of file From 63c0c2ab71bba116da6256b4b9ea9fc679d5a783 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Wed, 5 Nov 2025 15:26:34 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20DTO=EB=A5=BC=20common=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EB=A1=9C=20=EC=9D=B4=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commonmodule/dto/analysis/NaverProductDto.java | 12 ++++++++++++ .../commonmodule/dto/analysis/YoutubeVideoDto.java | 9 +++++++++ .../triple/inframodule/naver/NaverClient.java | 13 +------------ .../triple/inframodule/youtube/YoutubeClient.java | 10 +--------- 4 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/NaverProductDto.java create mode 100644 common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/YoutubeVideoDto.java diff --git a/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/NaverProductDto.java b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/NaverProductDto.java new file mode 100644 index 0000000..985f4ef --- /dev/null +++ b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/NaverProductDto.java @@ -0,0 +1,12 @@ +package hongik.triple.commonmodule.dto.analysis; + +public record NaverProductDto( + String productId, + String productName, + String productUrl, + Integer productPrice, + String productImage, + String categoryName, + String mallName, + String brand +) {} \ No newline at end of file diff --git a/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/YoutubeVideoDto.java b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/YoutubeVideoDto.java new file mode 100644 index 0000000..b6fbf34 --- /dev/null +++ b/common-module/src/main/java/hongik/triple/commonmodule/dto/analysis/YoutubeVideoDto.java @@ -0,0 +1,9 @@ +package hongik.triple.commonmodule.dto.analysis; + +public record YoutubeVideoDto( + String videoId, + String videoTitle, + String videoUrl, + String channelName, + String thumbnailUrl +) {} \ No newline at end of file diff --git a/infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.java b/infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.java index f7da817..cf6b3ae 100644 --- a/infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.java +++ b/infra-module/src/main/java/hongik/triple/inframodule/naver/NaverClient.java @@ -1,6 +1,7 @@ package hongik.triple.inframodule.naver; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import hongik.triple.commonmodule.dto.analysis.NaverProductDto; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -171,16 +172,4 @@ public static class NaverShoppingItem { private String category3; // 카테고리3 private String category4; // 카테고리4 } - - // Result DTO - public record NaverProductDto( - String productId, - String productName, - String productUrl, - Integer productPrice, - String productImage, - String categoryName, - String mallName, - String brand - ) {} } \ No newline at end of file diff --git a/infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.java b/infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.java index 67efa3f..c6f092c 100644 --- a/infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.java +++ b/infra-module/src/main/java/hongik/triple/inframodule/youtube/YoutubeClient.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import hongik.triple.commonmodule.dto.analysis.YoutubeVideoDto; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -149,13 +150,4 @@ public static class Thumbnails { public static class Thumbnail { private String url; } - - // Result DTO - public record YoutubeVideoDto( - String videoId, - String videoTitle, - String videoUrl, - String channelName, - String thumbnailUrl - ) {} } \ No newline at end of file From 971faaac5954144ebaefcfeed276cf44d78a4fb6 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Wed, 5 Nov 2025 15:26:49 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=ED=94=BC=EB=B6=80=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=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 | 30 ++++++++++++++--- .../analysis/AnalysisController.java | 4 +-- .../dto/analysis/AnalysisRes.java | 11 +++++++ .../commonmodule/enumerate/AcneType.java | 18 ++++++---- .../domain/analysis/Analysis.java | 33 ++++++++++++++++--- 5 files changed, 80 insertions(+), 16 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 4f66894..fe20d43 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 @@ -2,6 +2,9 @@ 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.enumerate.AcneType; import hongik.triple.domainmodule.domain.analysis.Analysis; import hongik.triple.domainmodule.domain.analysis.repository.AnalysisRepository; import hongik.triple.domainmodule.domain.member.Member; @@ -13,6 +16,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -34,18 +39,35 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { AnalysisData analysisData = aiClient.sendPredictRequest(multipartFile); // 진단 결과를 기반으로 피부 관리 영상 추천 (유튜브 API) - var videos = youtubeClient.searchVideos(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); + List videoList = + youtubeClient.searchVideos(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); // 진단 결과를 기반으로 맞춤형 제품 추천 (네이버 쇼핑 API) - var products = naverClient.searchProducts(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); + List productList = + naverClient.searchProducts(analysisData.labelToSkinType().getDescription() + " 피부 관리", 3); // DB 저장 - Analysis.builder() + Analysis analysis = Analysis.builder() .member(member) .acneType(analysisData.labelToSkinType()) + .imageUrl("S3 URL or other storage URL") + .isPublic(true) + .videoData(videoList) + .productData(productList) .build(); + Analysis saveAnalysis = analysisRepository.save(analysis); // Response - return new AnalysisRes(); // Replace with actual response data + return new AnalysisRes( + saveAnalysis.getAnalysisId(), + saveAnalysis.getImageUrl(), + saveAnalysis.getIsPublic(), + AcneType.valueOf(saveAnalysis.getAcneType()).name(), + AcneType.valueOf(saveAnalysis.getAcneType()).getDescription(), + AcneType.valueOf(saveAnalysis.getAcneType()).getCareMethod(), + AcneType.valueOf(saveAnalysis.getAcneType()).getGuide(), + videoList, + productList + ); } } 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 69c9d0e..3313c68 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 @@ -35,8 +35,8 @@ public class AnalysisController { @ApiResponse(responseCode = "500", description = "서버 오류") }) - public ApplicationResponse performAnalysis(@RequestPart(value = "file") MultipartFile multipartFile) { - return ApplicationResponse.ok(analysisService.performAnalysis(null, multipartFile)); + public ApplicationResponse performAnalysis(@AuthenticationPrincipal PrincipalDetails principalDetails, @RequestPart(value = "file") MultipartFile multipartFile) { + return ApplicationResponse.ok(analysisService.performAnalysis(principalDetails.getMember(), multipartFile)); } @GetMapping("/main") 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 e8b95cd..489a31c 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,5 +1,16 @@ package hongik.triple.commonmodule.dto.analysis; +import java.util.List; + public record AnalysisRes( + Long analysisId, + String imageUrl, + Boolean isPublic, + String acneType, + String description, + String careMethod, + String guide, + List videoList, + List productList ) { } diff --git a/common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java b/common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java index 209dcc4..de72f7e 100644 --- a/common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java +++ b/common-module/src/main/java/hongik/triple/commonmodule/enumerate/AcneType.java @@ -5,15 +5,21 @@ @Getter public enum AcneType { - NORMAL("정상"), - COMEDONES("좁쌀"), - PUSTULES("화농성"), - PAPULES("염증성"), - FOLLICULITIS("모낭염"); + NORMAL("정상", "피부 상태가 정상입니다.", "기본적인 세안과 보습을 유지하세요.", "건강한 식습관과 충분한 수면을 취하세요."), + COMEDONES("좁쌀", "모공이 막혀 형성된 작은 돌기입니다.", "과도한 피지 제거를 피하고, 순한 클렌저를 사용하세요.", "규칙적인 각질 제거와 보습을 유지하세요."), + PUSTULES("화농성", "피지선에 염증이 생겨 고름이 찬 여드름입니다.", "손으로 짜지 말고, 항염 작용이 있는 제품을 사용하세요.", "자극적인 음식과 스트레스를 피하세요."), + PAPULES("염증성", "붉고 단단한 여드름으로, 염증이 동반됩니다.", "항염 작용이 있는 스킨케어 제품을 사용하세요.", "피부를 청결하게 유지하고, 자극을 피하세요."), + FOLLICULITIS("모낭염", "모낭에 염증이 생긴 상태입니다.", "항생제 연고를 사용하고, 청결을 유지하세요.", "피부 자극을 최소화하고, 통풍이 잘 되는 옷을 입으세요."); + private final String koreanName; private final String description; + private final String careMethod; + private final String guide; - AcneType(String description) { + AcneType(String koreanName, String description, String careMethod, String guide) { + this.koreanName = koreanName; this.description = description; + this.careMethod = careMethod; + this.guide = guide; } } 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 cc43fa0..2cb3a17 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,8 +1,11 @@ package hongik.triple.domainmodule.domain.analysis; +import hongik.triple.commonmodule.dto.analysis.NaverProductDto; +import hongik.triple.commonmodule.dto.analysis.YoutubeVideoDto; import hongik.triple.commonmodule.enumerate.AcneType; import hongik.triple.domainmodule.common.BaseTimeEntity; import hongik.triple.domainmodule.domain.member.Member; +import hongik.triple.inframodule.youtube.YoutubeClient; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -12,6 +15,8 @@ import org.hibernate.annotations.SQLDelete; import org.hibernate.type.SqlTypes; +import java.util.List; + @Entity @Getter @Table(name = "analysis") @@ -28,16 +33,36 @@ public class Analysis extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Member member; + @Column(name = "image_url", nullable = false, columnDefinition = "text") + private String imageUrl; + @Column(name = "acne_type", nullable = false, length = 20) private String acneType; // @Enumerated(EnumType.STRING) 사용 X, String 형식으로 저장 -// @JdbcTypeCode(SqlTypes.JSON) -// @Column(name = "recommend_youtube", columnDefinition = "json") -// private RecommendYoutube recommendYoutube; + @Column(name = "is_public", nullable = false) + private Boolean isPublic; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "video_data", columnDefinition = "json") + private List videoData; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "product_data", columnDefinition = "json") + private List productData; + @Builder - public Analysis(Member member, AcneType acneType) { + public Analysis(Member member, + AcneType acneType, + String imageUrl, + Boolean isPublic, + List videoData, + List productData) { this.member = member; this.acneType = acneType.name(); + this.imageUrl = imageUrl; + this.isPublic = isPublic; + this.videoData = videoData; + this.productData = productData; } } From c947d70e7969e6f003abd68235e0a09f5653db53 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Wed, 5 Nov 2025 15:29:41 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20=ED=94=BC?= =?UTF-8?q?=EB=B6=80=20=EB=B6=84=EC=84=9D=20=EA=B2=B0=EA=B3=BC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/analysis/AnalysisService.java | 25 +++++++++++++++++++ .../analysis/AnalysisController.java | 4 +-- 2 files changed, 27 insertions(+), 2 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 fe20d43..70ae60d 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 @@ -35,6 +35,8 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { } // Business Logic + // TODO: S3 파일 업로드 + // 피부 분석 AI 모델 호출 AnalysisData analysisData = aiClient.sendPredictRequest(multipartFile); @@ -70,4 +72,27 @@ public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { productList ); } + + public AnalysisRes getAnalysisDetail(Member member, Long analysisId) { + // Validation + Analysis analysis = analysisRepository.findById(analysisId) + .orElseThrow(() -> new IllegalArgumentException("Analysis not found with id: " + analysisId)); + // Analysis가 요청한 사용자의 분석 결과인지 확인 + if(!analysis.getMember().getMemberId().equals(member.getMemberId())) { + throw new IllegalArgumentException("Unauthorized access to analysis with id: " + analysisId); + } + + // Response + return new AnalysisRes( + analysis.getAnalysisId(), + 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 3313c68..76f51ef 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 @@ -58,8 +58,8 @@ public ApplicationResponse getAnalysisListForMyPage() { } @GetMapping("/detail/{analysisId}") - public ApplicationResponse getAnalysisDetail(@PathVariable Long analysisId) { - return ApplicationResponse.ok(); + public ApplicationResponse getAnalysisDetail(@AuthenticationPrincipal PrincipalDetails principalDetails, @PathVariable Long analysisId) { + return ApplicationResponse.ok(analysisService.getAnalysisDetail(principalDetails.getMember(), analysisId)); } @GetMapping("/log") From 57cfcb1b1410eefbbcd2cb88187f87f7cd1a3012 Mon Sep 17 00:00:00 2001 From: dl-00-e8 Date: Wed, 5 Nov 2025 15:59:45 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/analysis/AnalysisService.java | 110 ++++++++++++++++++ .../analysis/AnalysisController.java | 16 ++- .../repository/AnalysisRepository.java | 20 ++++ 3 files changed, 140 insertions(+), 6 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 70ae60d..425acc4 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 @@ -12,6 +12,8 @@ import hongik.triple.inframodule.naver.NaverClient; import hongik.triple.inframodule.youtube.YoutubeClient; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -28,6 +30,7 @@ public class AnalysisService { private final NaverClient naverClient; private final AnalysisRepository analysisRepository; + @Transactional public AnalysisRes performAnalysis(Member member, MultipartFile multipartFile) { // Validation if(multipartFile.isEmpty() || multipartFile.getSize() == 0) { @@ -95,4 +98,111 @@ public AnalysisRes getAnalysisDetail(Member member, Long analysisId) { analysis.getProductData() ); } + + public List getAnalysisListForMainPage() { + // Business Logic + List analyses = analysisRepository.findTop3ByOrderByCreatedAtDesc(); + + // Response + return analyses.stream().map(analysis -> new AnalysisRes( + analysis.getAnalysisId(), + 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() + )).toList(); + } + + /** + * 피플즈 로그 페이지용 공개된 분석 기록 페이지네이션 조회 + * @param acneType 여드름 타입 (ALL인 경우 전체 조회) + * @param pageable 페이지 정보 + * @return 페이지네이션된 공개 분석 기록 리스트 + */ + public Page getAnalysisPaginationForLogPage(String acneType, Pageable pageable) { + // Validation + // acneType이 ALL이 아닌 경우 유효성 검증 + if (!"ALL".equalsIgnoreCase(acneType)) { + try { + AcneType.valueOf(acneType.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid acne type: " + acneType); + } + } + + // Business Logic + Page analysisPage; + + // "ALL"인 경우 전체 공개 분석 조회, 아니면 타입별 공개 분석 조회 + if ("ALL".equalsIgnoreCase(acneType)) { + analysisPage = analysisRepository.findByIsPublicTrueOrderByCreatedAtDesc(pageable); + } else { + analysisPage = analysisRepository.findByIsPublicTrueAndAcneTypeOrderByCreatedAtDesc( + acneType.toUpperCase(), pageable); + } + + // Response + return analysisPage.map(analysis -> new AnalysisRes( + analysis.getAnalysisId(), + 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() + )); + } + + /** + * 마이페이지용 내 분석 기록 페이지네이션 조회 + * @param member 현재 로그인한 회원 + * @param acneType 여드름 타입 (ALL인 경우 전체 조회) + * @param pageable 페이지 정보 + * @return 페이지네이션된 내 분석 기록 리스트 + */ + public Page getAnalysisListForMyPage(Member member, String acneType, Pageable pageable) { + // Validation + if (member == null) { + throw new IllegalArgumentException("Member cannot be null"); + } + + // acneType이 ALL이 아닌 경우 유효성 검증 + if (!"ALL".equalsIgnoreCase(acneType)) { + try { + AcneType.valueOf(acneType.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid acne type: " + acneType); + } + } + + // Business Logic + Page analysisPage; + + // "ALL"인 경우 내 전체 분석 조회, 아니면 타입별 내 분석 조회 + if ("ALL".equalsIgnoreCase(acneType)) { + analysisPage = analysisRepository.findByMemberOrderByCreatedAtDesc(member, pageable); + } else { + analysisPage = analysisRepository.findByMemberAndAcneTypeOrderByCreatedAtDesc( + member, acneType.toUpperCase(), pageable); + } + + // Response + return analysisPage.map(analysis -> new AnalysisRes( + analysis.getAnalysisId(), + 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 76f51ef..9e665f2 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 @@ -3,7 +3,6 @@ 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; @@ -13,6 +12,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -49,12 +50,14 @@ public ApplicationResponse performAnalysis(@AuthenticationPrincipal Principal description = "서버 오류") }) public ApplicationResponse getAnalysisListForMainPage() { - return ApplicationResponse.ok(); + return ApplicationResponse.ok(analysisService.getAnalysisListForMainPage()); } @GetMapping("/my") - public ApplicationResponse getAnalysisListForMyPage() { - return ApplicationResponse.ok(); + 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}") @@ -63,7 +66,8 @@ public ApplicationResponse getAnalysisDetail(@AuthenticationPrincipal Princip } @GetMapping("/log") - public ApplicationResponse getAnalysisPaginationForLogPage() { - return ApplicationResponse.ok(); + public ApplicationResponse getAnalysisPaginationForLogPage(@RequestParam(name = "type") String acneType, + @PageableDefault(size = 4) Pageable pageable) { + return ApplicationResponse.ok(analysisService.getAnalysisPaginationForLogPage(acneType, pageable)); } } 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 31cd960..e2445a5 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 @@ -1,7 +1,27 @@ package hongik.triple.domainmodule.domain.analysis.repository; import hongik.triple.domainmodule.domain.analysis.Analysis; +import hongik.triple.domainmodule.domain.member.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface AnalysisRepository extends JpaRepository { + + // 메인 페이지용 + List findTop3ByOrderByCreatedAtDesc(); + + // 피플즈 로그 페이지용 - 전체 공개 분석 조회 + Page findByIsPublicTrueOrderByCreatedAtDesc(Pageable pageable); + + // 피플즈 로그 페이지용 - 타입별 공개 분석 조회 + Page findByIsPublicTrueAndAcneTypeOrderByCreatedAtDesc(String acneType, Pageable pageable); + + // 마이페이지용 - 내 전체 분석 조회 + Page findByMemberOrderByCreatedAtDesc(Member member, Pageable pageable); + + // 마이페이지용 - 내 타입별 분석 조회 + Page findByMemberAndAcneTypeOrderByCreatedAtDesc(Member member, String acneType, Pageable pageable); }