Skip to content

Commit f35b4c7

Browse files
authored
Merge pull request #151 from TeamMody/feature/#150
✨ [FEATURE] #150 - 오늘 날씨에 어울리는 패션 추천 API
2 parents 6ac1862 + 5e02cb1 commit f35b4c7

File tree

9 files changed

+373
-12
lines changed

9 files changed

+373
-12
lines changed

src/main/java/com/example/mody/domain/chatgpt/service/ChatGptService.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import com.example.mody.domain.bodytype.dto.response.BodyTypeAnalysisResponse;
44
import com.example.mody.domain.recommendation.dto.request.MemberInfoRequest;
5+
import com.example.mody.domain.recommendation.dto.request.WeatherRecommendRequest;
56
import com.example.mody.domain.recommendation.dto.response.analysis.ItemAnalysisResponse;
67
import com.example.mody.domain.member.enums.Gender;
78
import com.example.mody.domain.recommendation.dto.request.RecommendRequest;
89
import com.example.mody.domain.recommendation.dto.response.analysis.StyleAnalysisResponse;
10+
import com.example.mody.domain.recommendation.dto.response.analysis.WeatherStyleAnalysisResponse;
911
import com.example.mody.domain.recommendation.service.CrawlerService;
1012
import lombok.extern.slf4j.Slf4j;
1113
import org.springframework.beans.factory.annotation.Value;
@@ -120,17 +122,6 @@ public StyleAnalysisResponse recommendGptStyle(MemberInfoRequest memberInfoReque
120122
}
121123
}
122124

123-
private String searchImageFromPinterest(Gender gender, String recommendedStyle) {
124-
String strGender = (gender == Gender.MALE) ? "남성 " : "여성 ";
125-
String keyword = strGender + recommendedStyle;
126-
log.info("keyword: {}", keyword);
127-
128-
String imageUrl = crawlerService.getRandomImageUrl(keyword);
129-
log.info("Pinterest 이미지 URL: {}", imageUrl);
130-
131-
return imageUrl;
132-
}
133-
134125
// 패션 아이템 추천 메서드
135126
public ItemAnalysisResponse recommendGptItem(MemberInfoRequest memberInfoRequest, RecommendRequest recommendRequest){
136127

@@ -156,4 +147,44 @@ public ItemAnalysisResponse recommendGptItem(MemberInfoRequest memberInfoRequest
156147
throw new RestApiException(AnalysisErrorStatus._GPT_ERROR);
157148
}
158149
}
150+
151+
// 오늘 날씨에 어울리는 패션 추천
152+
public WeatherStyleAnalysisResponse recommendWeatherStyle(
153+
MemberInfoRequest memberInfoRequest,
154+
WeatherRecommendRequest weatherRecommendRequest) {
155+
156+
// 프롬프트 생성
157+
String prompt = promptManager.createWeatherStyleRecommendation(memberInfoRequest, weatherRecommendRequest);
158+
159+
// OpenAI 답변 생성
160+
ChatGPTResponse response = openAiApiClient.sendRequestToModel(
161+
model,
162+
List.of(
163+
new Message(systemRole, prompt)
164+
),
165+
maxTokens,
166+
temperature);
167+
String content = response.getChoices().get(0).getMessage().getContent().trim();
168+
169+
try{
170+
WeatherStyleAnalysisResponse weatherStyleAnalysisResponse = objectMapper.readValue(content, WeatherStyleAnalysisResponse.class);
171+
return weatherStyleAnalysisResponse.from(searchImageFromPinterest(memberInfoRequest.getGender(), weatherStyleAnalysisResponse.getConcept()));
172+
} catch (JsonMappingException e) {
173+
throw new RestApiException(AnalysisErrorStatus._GPT_ERROR);
174+
} catch (JsonProcessingException e) {
175+
throw new RestApiException(AnalysisErrorStatus._GPT_ERROR);
176+
}
177+
}
178+
179+
// 핀터레스트 사진 크롤링
180+
private String searchImageFromPinterest(Gender gender, String recommendedStyle) {
181+
String strGender = (gender == Gender.MALE) ? "남성 " : "여성 ";
182+
String keyword = strGender + recommendedStyle;
183+
log.info("keyword: {}", keyword);
184+
185+
String imageUrl = crawlerService.getRandomImageUrl(keyword);
186+
log.info("Pinterest 이미지 URL: {}", imageUrl);
187+
188+
return imageUrl;
189+
}
159190
}

src/main/java/com/example/mody/domain/recommendation/controller/RecommendationController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.example.mody.domain.auth.security.CustomUserDetails;
44
import com.example.mody.domain.recommendation.dto.request.RecommendRequest;
5+
import com.example.mody.domain.recommendation.dto.request.WeatherRecommendRequest;
56
import com.example.mody.domain.recommendation.dto.response.CategoryResponse;
67
import com.example.mody.domain.recommendation.dto.response.RecommendLikeResponse;
78
import com.example.mody.domain.recommendation.dto.response.RecommendResponse;
@@ -75,4 +76,15 @@ public BaseResponse<RecommendResponse> recommendFashionItem(
7576
customUserDetails.getMember(), request);
7677
return BaseResponse.onSuccess(response);
7778
}
79+
80+
// 오늘의 날씨에 어울리는 패션 추천 API
81+
@PostMapping("/weather")
82+
public BaseResponse<RecommendResponse> recommendWeatherStyle(
83+
@Valid @RequestBody WeatherRecommendRequest request,
84+
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
85+
86+
RecommendResponse response = recommendationCommendService.recommendWeatherStyle(
87+
customUserDetails.getMember(), request);
88+
return BaseResponse.onSuccess(response);
89+
}
7890
}

src/main/java/com/example/mody/domain/recommendation/controller/RecommendationControllerInterface.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.example.mody.domain.auth.security.CustomUserDetails;
44
import com.example.mody.domain.recommendation.dto.request.RecommendRequest;
5+
import com.example.mody.domain.recommendation.dto.request.WeatherRecommendRequest;
56
import com.example.mody.domain.recommendation.dto.response.CategoryResponse;
67
import com.example.mody.domain.recommendation.dto.response.RecommendLikeResponse;
78
import com.example.mody.domain.recommendation.dto.response.RecommendResponse;
@@ -13,6 +14,8 @@
1314
import io.swagger.v3.oas.annotations.media.Schema;
1415
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1516
import io.swagger.v3.oas.annotations.responses.ApiResponses;
17+
import jakarta.validation.Valid;
18+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1619
import org.springframework.web.bind.annotation.*;
1720

1821
public interface RecommendationControllerInterface {
@@ -195,4 +198,68 @@ BaseResponse<RecommendResponse> recommendStyle(
195198
BaseResponse<RecommendResponse> recommendFashionItem(
196199
RecommendRequest request,
197200
CustomUserDetails customUserDetails);
201+
202+
@Operation(summary = "오늘 날씨에 어울리는 패션 추천 API", description = "오늘 날씨를 키워드로 입력해 사용자 맞춤 패션을 추천받는 API입니다.")
203+
@ApiResponses({
204+
@ApiResponse(
205+
responseCode = "200",
206+
description = "오늘 날씨에 어울리는 패션 추천 성공",
207+
content = @Content(schema = @Schema(implementation = RecommendResponse.class))
208+
),
209+
@ApiResponse(
210+
responseCode = "MEMBER_BODY_TYPE404",
211+
description = "사용자의 체형 정보를 찾을 수 없음",
212+
content = @Content(
213+
mediaType = "application/json",
214+
examples = @ExampleObject(
215+
value = """
216+
{
217+
"timestamp": "2025-01-17T00:48:53.9237864",
218+
"code": "MEMBER_BODY_TYPE404",
219+
"message": "체형 분석 결과를 찾을 수 없습니다."
220+
}
221+
"""
222+
)
223+
)
224+
),
225+
@ApiResponse(
226+
responseCode = "COMMON402",
227+
description = "카테고리 리스트가 비어있을 때 발생합니다. " +
228+
"선호하는 스타일/ 선호하지 않는 스타일/ 보여주고 싶은 이미지 목록으로 표시됩니다.",
229+
content = @Content(
230+
mediaType = "application/json",
231+
examples = @ExampleObject(
232+
value = """
233+
{
234+
"timestamp": "2025-01-25T15:57:08.7901651",
235+
"code": "COMMON402",
236+
"message": "Validation Error입니다.",
237+
"result": {
238+
"preferredStyles": "선호하는 스타일 목록은 비어 있을 수 없습니다."
239+
}
240+
}
241+
"""
242+
)
243+
)
244+
),
245+
@ApiResponse(
246+
responseCode = "ANALYSIS108",
247+
description = "GPT 응답 형식이 적절하지 않을 때 발생합니다.",
248+
content = @Content(
249+
mediaType = "application/json",
250+
examples = @ExampleObject(
251+
value = """
252+
{
253+
"timestamp": "2025-01-25T16:02:42.4014717",
254+
"code": "ANALYSIS108",
255+
"message": "GPT가 올바르지 않은 답변을 했습니다. 관리자에게 문의하세요."
256+
}
257+
"""
258+
)
259+
)
260+
)
261+
})
262+
BaseResponse<RecommendResponse> recommendWeatherStyle(
263+
@Valid @RequestBody WeatherRecommendRequest request,
264+
@AuthenticationPrincipal CustomUserDetails customUserDetails);
198265
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.example.mody.domain.recommendation.dto.request;
2+
3+
import com.example.mody.global.common.exception.annotation.IsEmptyList;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import jakarta.validation.constraints.NotNull;
6+
import lombok.Data;
7+
import lombok.Getter;
8+
9+
import java.util.List;
10+
11+
@Schema(description = "스타일 추천 요청 DTO")
12+
@Data
13+
@Getter
14+
public class WeatherRecommendRequest {
15+
16+
@Schema(description = "오늘의 날씨", example = "비")
17+
String weather;
18+
19+
@Schema(
20+
description = "선호하는 스타일",
21+
example = "[\"힙/스트릿\",\"빈티지\"]"
22+
)
23+
@NotNull(message = "선호하는 스타일은 필수 항목입니다.")
24+
@IsEmptyList(message = "선호하는 스타일 목록은 비어 있을 수 없습니다.")
25+
private List<String> preferredStyles;
26+
27+
@Schema(
28+
description = "사용자가 선호하지 않는 스타일 목록 (예: 포멀 등)",
29+
example = "[\"페미닌\", \"러블리\"]"
30+
)
31+
@NotNull(message = "선호하지 않는 스타일은 필수 항목입니다.")
32+
@IsEmptyList(message = "선호하지 않는 스타일 목록은 비어 있을 수 없습니다.")
33+
private List<String> dislikedStyles;
34+
35+
@Schema(
36+
description = "사용자가 보여주고 싶은 이미지 설명 (예: 세련되고 자유로운 이미지)",
37+
example = "[\"섹시한\", \"시크한\"]"
38+
)
39+
@NotNull(message = "보여주고 싶은 이미지는 필수 항목입니다.")
40+
@IsEmptyList(message = "보여주고 싶은 이미지 목록은 비어 있을 수 없습니다.")
41+
private List<String> appealedImage;
42+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.example.mody.domain.recommendation.dto.response.analysis;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.*;
5+
import java.util.List;
6+
7+
@Schema(description = "오늘 날씨에 어울리는 패션 추천 응답 정보")
8+
@Getter
9+
@Builder(toBuilder = true)
10+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
11+
@AllArgsConstructor
12+
public class WeatherStyleAnalysisResponse {
13+
14+
@Schema(description = "추천 컨셉", example = "비 오는 날의 스트릿 & 힙 스타일")
15+
private String concept;
16+
17+
@Schema(description = "이미지 URL",
18+
example = "https://i.pinimg.com/236x/32/87/f8/3287f86756200b3c8d9d28181aaddeae.jpg")
19+
private String imageUrl;
20+
21+
@Schema(description = "스타일 방향 정보")
22+
private StyleDirection styleDirection;
23+
24+
@Schema(description = "패션 팁 목록")
25+
private List<String> weatherTip;
26+
27+
@Schema(description = "추천 스타일링 정보")
28+
private RecommendedStyling recommendedStyling;
29+
30+
@Schema(description = "스타일 방향 정보")
31+
@Getter
32+
@Builder(toBuilder = true)
33+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
34+
@AllArgsConstructor
35+
public static class StyleDirection {
36+
@Schema(description = "설명",
37+
example = "비가 오는 날에 적합하면서도 힙한 스트릿 스타일을 유지할 수 있는 패션을 제안합니다.")
38+
private String explanation;
39+
40+
@Schema(description = "스타일 선호도",
41+
example = "스트레이트 체형을 최대한 돋보이게 하면서 개성을 살린 스타일링을 추천합니다.")
42+
private String stylePreference;
43+
44+
@Schema(description = "날씨 적응",
45+
example = "방수 소재와 기능적인 디자인을 활용하여 비를 막고, 동시에 패션 감각도 유지할 수 있습니다.")
46+
private String weatherAdaptation;
47+
}
48+
49+
@Schema(description = "추천 스타일링 정보")
50+
@Getter
51+
@Builder(toBuilder = true)
52+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
53+
@AllArgsConstructor
54+
public static class RecommendedStyling {
55+
@Schema(description = "제목", example = "비 오는 날의 힙한 스트릿웨어")
56+
private String title;
57+
58+
@Schema(description = "스타일 설명",
59+
example = "오버사이즈 방수 재킷과 편안한 핏의 팬츠를 매치하여 활동성과 방수 기능을 모두 갖춘 스타일을 연출하세요. 스니커즈나 방수 부츠와 함께하시면 실용성과 스타일 모두를 챙길 수 있습니다.")
60+
private String styleDescription;
61+
62+
@Schema(description = "선택 이유",
63+
example = "이 스타일은 비 오는 날에도 활동적이면서도 자기만의 힙한 스타일을 표현할 수 있는 최적의 선택입니다. 스트레이트 체형은 오버사이즈 아이템과 잘 어울리며, 편안하게 스타일링할 수 있습니다.")
64+
private String reason;
65+
}
66+
67+
public WeatherStyleAnalysisResponse from(String imageUrl) {
68+
return this.toBuilder()
69+
.imageUrl(imageUrl)
70+
.build();
71+
}
72+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package com.example.mody.domain.recommendation.enums;
22

33
public enum RecommendType {
4-
STYLE, FASHION_ITEM
4+
STYLE, FASHION_ITEM, WEATHER, OCCASION;
55
}

src/main/java/com/example/mody/domain/recommendation/service/RecommendationCommendService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
import com.example.mody.domain.member.entity.Member;
44
import com.example.mody.domain.recommendation.dto.request.RecommendRequest;
5+
import com.example.mody.domain.recommendation.dto.request.WeatherRecommendRequest;
56
import com.example.mody.domain.recommendation.dto.response.RecommendResponse;
67
import com.example.mody.domain.recommendation.dto.response.RecommendLikeResponse;
8+
import com.example.mody.domain.recommendation.dto.response.analysis.WeatherStyleAnalysisResponse;
79

810
public interface RecommendationCommendService {
911

1012
RecommendResponse recommendStyle(Member member, RecommendRequest request);
1113

1214
RecommendResponse recommendFashionItem(Member member, RecommendRequest request);
1315

16+
RecommendResponse recommendWeatherStyle(Member member, WeatherRecommendRequest request);
17+
1418
RecommendLikeResponse toggleLike(Long recommendationId, Member member);
1519
}

0 commit comments

Comments
 (0)