From 4946be1f49e1781bdc6783dab5db95c84043be97 Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Thu, 12 Jun 2025 18:15:10 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20:=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/service/StoreRecommendInteractor.java | 148 +++++++++--------- .../dto/response/RecommendStoreResponse.java | 3 + .../dto/response/StoreDetailResponse.java | 70 ++++++--- .../dto/response/TasteAnalysisResponse.java | 7 + 4 files changed, 133 insertions(+), 95 deletions(-) diff --git a/recommend/src/main/java/com/ktds/hi/recommend/biz/service/StoreRecommendInteractor.java b/recommend/src/main/java/com/ktds/hi/recommend/biz/service/StoreRecommendInteractor.java index 5935d4b..26b4c10 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/biz/service/StoreRecommendInteractor.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/biz/service/StoreRecommendInteractor.java @@ -16,6 +16,10 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; + + + + /** * 매장 추천 인터랙터 클래스 * 사용자 취향 기반 매장 추천 기능을 구현 @@ -25,108 +29,108 @@ import java.util.stream.Collectors; @Slf4j @Transactional public class StoreRecommendInteractor implements StoreRecommendUseCase { - + private final RecommendRepository recommendRepository; private final AiRecommendRepository aiRecommendRepository; private final LocationRepository locationRepository; private final UserPreferenceRepository userPreferenceRepository; - + @Override public List recommendStores(Long memberId, RecommendStoreRequest request) { // 사용자 취향 프로필 조회 TasteProfile tasteProfile = userPreferenceRepository.getMemberPreferences(memberId) - .orElseThrow(() -> new BusinessException("사용자 취향 정보를 찾을 수 없습니다. 취향 등록을 먼저 해주세요.")); - + .orElseThrow(() -> new BusinessException("사용자 취향 정보를 찾을 수 없습니다. 취향 등록을 먼저 해주세요.")); + // AI 기반 추천 Map preferences = Map.of( - "categories", tasteProfile.getPreferredCategories(), - "tags", tasteProfile.getPreferredTags(), - "pricePreference", tasteProfile.getPricePreference(), - "distancePreference", tasteProfile.getDistancePreference(), - "latitude", request.getLatitude(), - "longitude", request.getLongitude() + "categories", tasteProfile.getPreferredCategories(), + "tags", tasteProfile.getPreferredTags(), + "pricePreference", tasteProfile.getPricePreference(), + "distancePreference", tasteProfile.getDistancePreference(), + "latitude", request.getLatitude(), + "longitude", request.getLongitude() ); - + List aiRecommendStores = aiRecommendRepository.recommendStoresByAI(memberId, preferences); - + // 위치 기반 추천 결합 List locationStores = locationRepository.findStoresWithinRadius( - request.getLatitude(), request.getLongitude(), request.getRadius()); - + request.getLatitude(), request.getLongitude(), request.getRadius()); + // 추천 결과 통합 및 점수 계산 List combinedStores = combineRecommendations(aiRecommendStores, locationStores, tasteProfile); - + // 추천 히스토리 저장 RecommendHistory history = RecommendHistory.builder() - .memberId(memberId) - .recommendedStoreIds(combinedStores.stream().map(RecommendStore::getStoreId).collect(Collectors.toList())) - .recommendType(RecommendType.TASTE_BASED) - .criteria("취향 + AI + 위치 기반 통합 추천") - .createdAt(LocalDateTime.now()) - .build(); - + .memberId(memberId) + .recommendedStoreIds(combinedStores.stream().map(RecommendStore::getStoreId).collect(Collectors.toList())) + .recommendType(RecommendType.TASTE_BASED) + .criteria("취향 + AI + 위치 기반 통합 추천") + .createdAt(LocalDateTime.now()) + .build(); + recommendRepository.saveRecommendHistory(history); - + log.info("매장 추천 완료: memberId={}, 추천 매장 수={}", memberId, combinedStores.size()); - + return combinedStores.stream() - .map(this::toRecommendStoreResponse) - .collect(Collectors.toList()); + .map(this::toRecommendStoreResponse) + .collect(Collectors.toList()); } - + @Override @Transactional(readOnly = true) public List recommendStoresByLocation(Double latitude, Double longitude, Integer radius) { List stores = locationRepository.findStoresWithinRadius(latitude, longitude, radius); - + return stores.stream() - .map(store -> store.updateRecommendReason("위치 기반 추천")) - .map(this::toRecommendStoreResponse) - .collect(Collectors.toList()); + .map(store -> store.updateRecommendReason("위치 기반 추천")) + .map(this::toRecommendStoreResponse) + .collect(Collectors.toList()); } - + @Override @Transactional(readOnly = true) public List recommendPopularStores(String category, Integer limit) { // Mock 구현 - 실제로는 인기도 기반 쿼리 필요 List popularStores = List.of( - RecommendStore.builder() - .storeId(1L) - .storeName("인기 매장 1") - .address("서울시 강남구") - .category(category) - .rating(4.5) - .reviewCount(100) - .recommendScore(95.0) - .recommendType(RecommendType.POPULARITY_BASED) - .recommendReason("높은 평점과 많은 리뷰") - .build() + RecommendStore.builder() + .storeId(1L) + .storeName("인기 매장 1") + .address("서울시 강남구") + .category(category) + .rating(4.5) + .reviewCount(100) + .recommendScore(95.0) + .recommendType(RecommendType.POPULARITY_BASED) + .recommendReason("높은 평점과 많은 리뷰") + .build() ); - + return popularStores.stream() - .limit(limit != null ? limit : 10) - .map(this::toRecommendStoreResponse) - .collect(Collectors.toList()); + .limit(limit != null ? limit : 10) + .map(this::toRecommendStoreResponse) + .collect(Collectors.toList()); } - + /** * 추천 결과 통합 및 점수 계산 */ - private List combineRecommendations(List aiStores, - List locationStores, - TasteProfile profile) { + private List combineRecommendations(List aiStores, + List locationStores, + TasteProfile profile) { // AI 추천과 위치 기반 추천을 통합하여 최종 점수 계산 // 실제로는 더 복잡한 로직이 필요 - + return aiStores.stream() - .map(store -> store.updateRecommendScore( - calculateFinalScore(store, profile) - )) - .sorted((s1, s2) -> Double.compare(s2.getRecommendScore(), s1.getRecommendScore())) - .limit(20) - .collect(Collectors.toList()); + .map(store -> store.updateRecommendScore( + calculateFinalScore(store, profile) + )) + .sorted((s1, s2) -> Double.compare(s2.getRecommendScore(), s1.getRecommendScore())) + .limit(20) + .collect(Collectors.toList()); } - + /** * 최종 추천 점수 계산 */ @@ -135,25 +139,25 @@ public class StoreRecommendInteractor implements StoreRecommendUseCase { double ratingScore = store.getRating() != null ? store.getRating() * 10 : 0.0; double reviewScore = store.getReviewCount() != null ? Math.min(store.getReviewCount() * 0.1, 10) : 0.0; double distanceScore = store.getDistance() != null ? Math.max(0, 10 - store.getDistance() / 1000) : 0.0; - + return (baseScore * 0.4) + (ratingScore * 0.3) + (reviewScore * 0.2) + (distanceScore * 0.1); } - + /** * 도메인을 응답 DTO로 변환 */ private RecommendStoreResponse toRecommendStoreResponse(RecommendStore store) { return RecommendStoreResponse.builder() - .storeId(store.getStoreId()) - .storeName(store.getStoreName()) - .address(store.getAddress()) - .category(store.getCategory()) - .tags(store.getTags()) - .rating(store.getRating()) - .reviewCount(store.getReviewCount()) - .distance(store.getDistance()) - .recommendScore(store.getRecommendScore()) - .recommendReason(store.getRecommendReason()) - .build(); + .storeId(store.getStoreId()) + .storeName(store.getStoreName()) + .address(store.getAddress()) + .category(store.getCategory()) + .tags(store.getTags()) + .rating(store.getRating()) + .reviewCount(store.getReviewCount()) + .distance(store.getDistance()) + .recommendScore(store.getRecommendScore()) + .recommendReason(store.getRecommendReason()) + .build(); } } diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java index fae0e29..47ded4c 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java @@ -52,4 +52,7 @@ public class RecommendStoreResponse { @Schema(description = "태그 목록", example = "[\"매운맛\", \"혼밥\"]") private List tags; + + @Schema(description = "가게 점수") + private Double rating; } diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/StoreDetailResponse.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/StoreDetailResponse.java index b0c6c63..6e3d763 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/StoreDetailResponse.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/StoreDetailResponse.java @@ -17,42 +17,66 @@ public class StoreDetailResponse { @Schema(description = "매장 ID", example = "1") private Long storeId; - @Schema(description = "매장명", example = "맛있는 김치찌개") + @Schema(description = "매장명", example = "맛있는 한식당") private String storeName; - @Schema(description = "카테고리", example = "한식") - private String category; - @Schema(description = "주소", example = "서울시 강남구 테헤란로 123") private String address; - @Schema(description = "전화번호", example = "02-1234-5678") - private String phoneNumber; + @Schema(description = "카테고리", example = "한식") + private String category; - @Schema(description = "위도", example = "37.5665") - private Double latitude; + @Schema(description = "평점", example = "4.5") + private Double rating; - @Schema(description = "경도", example = "126.9780") - private Double longitude; - - @Schema(description = "평균 평점", example = "4.5") - private Double averageRating; - - @Schema(description = "리뷰 수", example = "127") + @Schema(description = "리뷰 수", example = "256") private Integer reviewCount; + @Schema(description = "거리(미터)", example = "500") + private Integer distance; + + @Schema(description = "태그 목록", example = "[\"맛집\", \"혼밥\", \"가성비\"]") + private List tags; + @Schema(description = "매장 이미지 URL 목록") - private List imageUrls; + private List images; - @Schema(description = "운영시간", example = "10:00-22:00") - private String operatingHours; + @Schema(description = "매장 설명") + private String description; - @Schema(description = "AI 요약", example = "친절한 서비스와 맛있는 김치찌개로 유명한 곳입니다") + @Schema(description = "개인화된 추천 이유", example = "당신이 좋아하는 '매운맛' 특성을 가진 매장입니다") + private String personalizedReason; + + @Schema(description = "AI 요약", example = "고객들이 매운맛과 친절한 서비스를 칭찬하는 매장입니다") private String aiSummary; - @Schema(description = "긍정 키워드", example = "[\"맛있다\", \"친절하다\", \"깔끔하다\"]") - private List topPositiveKeywords; + @Schema(description = "운영 시간") + private String operatingHours; - @Schema(description = "부정 키워드", example = "[\"시끄럽다\", \"대기시간\"]") - private List topNegativeKeywords; + @Schema(description = "전화번호") + private String phoneNumber; + + @Schema(description = "메뉴 정보") + private List menuList; + + /** + * 메뉴 정보 내부 클래스 + */ + @Getter + @Builder + @Schema(description = "메뉴 정보") + public static class MenuInfo { + + @Schema(description = "메뉴명", example = "김치찌개") + private String menuName; + + @Schema(description = "가격", example = "8000") + private Integer price; + + @Schema(description = "메뉴 설명", example = "매콤한 김치찌개") + private String description; + + @Schema(description = "인기 메뉴 여부", example = "true") + private Boolean isPopular; + } } \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java index f20798a..dd3770b 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java @@ -31,6 +31,12 @@ public class TasteAnalysisResponse { @Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\"]") private List preferredTags; + @Schema(description = "가격 선호도 (0-100 점수)", example = "65.5") + private Double pricePreference; + + @Schema(description = "거리 선호도 (0-100 점수)", example = "70.0") + private Double distancePreference; + @Schema(description = "분석 일시") private LocalDateTime analysisDate; @@ -39,4 +45,5 @@ public class TasteAnalysisResponse { @Schema(description = "추천사항", example = "[\"한식 카테고리의 새로운 매장을 시도해보세요\"]") private List recommendations; + }