Fix : 수정

This commit is contained in:
lsh9672 2025-06-12 18:15:10 +09:00
parent 28d34dba8b
commit 4946be1f49
4 changed files with 133 additions and 95 deletions

View File

@ -16,6 +16,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 매장 추천 인터랙터 클래스 * 매장 추천 인터랙터 클래스
* 사용자 취향 기반 매장 추천 기능을 구현 * 사용자 취향 기반 매장 추천 기능을 구현
@ -25,108 +29,108 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
@Transactional @Transactional
public class StoreRecommendInteractor implements StoreRecommendUseCase { public class StoreRecommendInteractor implements StoreRecommendUseCase {
private final RecommendRepository recommendRepository; private final RecommendRepository recommendRepository;
private final AiRecommendRepository aiRecommendRepository; private final AiRecommendRepository aiRecommendRepository;
private final LocationRepository locationRepository; private final LocationRepository locationRepository;
private final UserPreferenceRepository userPreferenceRepository; private final UserPreferenceRepository userPreferenceRepository;
@Override @Override
public List<RecommendStoreResponse> recommendStores(Long memberId, RecommendStoreRequest request) { public List<RecommendStoreResponse> recommendStores(Long memberId, RecommendStoreRequest request) {
// 사용자 취향 프로필 조회 // 사용자 취향 프로필 조회
TasteProfile tasteProfile = userPreferenceRepository.getMemberPreferences(memberId) TasteProfile tasteProfile = userPreferenceRepository.getMemberPreferences(memberId)
.orElseThrow(() -> new BusinessException("사용자 취향 정보를 찾을 수 없습니다. 취향 등록을 먼저 해주세요.")); .orElseThrow(() -> new BusinessException("사용자 취향 정보를 찾을 수 없습니다. 취향 등록을 먼저 해주세요."));
// AI 기반 추천 // AI 기반 추천
Map<String, Object> preferences = Map.of( Map<String, Object> preferences = Map.of(
"categories", tasteProfile.getPreferredCategories(), "categories", tasteProfile.getPreferredCategories(),
"tags", tasteProfile.getPreferredTags(), "tags", tasteProfile.getPreferredTags(),
"pricePreference", tasteProfile.getPricePreference(), "pricePreference", tasteProfile.getPricePreference(),
"distancePreference", tasteProfile.getDistancePreference(), "distancePreference", tasteProfile.getDistancePreference(),
"latitude", request.getLatitude(), "latitude", request.getLatitude(),
"longitude", request.getLongitude() "longitude", request.getLongitude()
); );
List<RecommendStore> aiRecommendStores = aiRecommendRepository.recommendStoresByAI(memberId, preferences); List<RecommendStore> aiRecommendStores = aiRecommendRepository.recommendStoresByAI(memberId, preferences);
// 위치 기반 추천 결합 // 위치 기반 추천 결합
List<RecommendStore> locationStores = locationRepository.findStoresWithinRadius( List<RecommendStore> locationStores = locationRepository.findStoresWithinRadius(
request.getLatitude(), request.getLongitude(), request.getRadius()); request.getLatitude(), request.getLongitude(), request.getRadius());
// 추천 결과 통합 점수 계산 // 추천 결과 통합 점수 계산
List<RecommendStore> combinedStores = combineRecommendations(aiRecommendStores, locationStores, tasteProfile); List<RecommendStore> combinedStores = combineRecommendations(aiRecommendStores, locationStores, tasteProfile);
// 추천 히스토리 저장 // 추천 히스토리 저장
RecommendHistory history = RecommendHistory.builder() RecommendHistory history = RecommendHistory.builder()
.memberId(memberId) .memberId(memberId)
.recommendedStoreIds(combinedStores.stream().map(RecommendStore::getStoreId).collect(Collectors.toList())) .recommendedStoreIds(combinedStores.stream().map(RecommendStore::getStoreId).collect(Collectors.toList()))
.recommendType(RecommendType.TASTE_BASED) .recommendType(RecommendType.TASTE_BASED)
.criteria("취향 + AI + 위치 기반 통합 추천") .criteria("취향 + AI + 위치 기반 통합 추천")
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now())
.build(); .build();
recommendRepository.saveRecommendHistory(history); recommendRepository.saveRecommendHistory(history);
log.info("매장 추천 완료: memberId={}, 추천 매장 수={}", memberId, combinedStores.size()); log.info("매장 추천 완료: memberId={}, 추천 매장 수={}", memberId, combinedStores.size());
return combinedStores.stream() return combinedStores.stream()
.map(this::toRecommendStoreResponse) .map(this::toRecommendStoreResponse)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<RecommendStoreResponse> recommendStoresByLocation(Double latitude, Double longitude, Integer radius) { public List<RecommendStoreResponse> recommendStoresByLocation(Double latitude, Double longitude, Integer radius) {
List<RecommendStore> stores = locationRepository.findStoresWithinRadius(latitude, longitude, radius); List<RecommendStore> stores = locationRepository.findStoresWithinRadius(latitude, longitude, radius);
return stores.stream() return stores.stream()
.map(store -> store.updateRecommendReason("위치 기반 추천")) .map(store -> store.updateRecommendReason("위치 기반 추천"))
.map(this::toRecommendStoreResponse) .map(this::toRecommendStoreResponse)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<RecommendStoreResponse> recommendPopularStores(String category, Integer limit) { public List<RecommendStoreResponse> recommendPopularStores(String category, Integer limit) {
// Mock 구현 - 실제로는 인기도 기반 쿼리 필요 // Mock 구현 - 실제로는 인기도 기반 쿼리 필요
List<RecommendStore> popularStores = List.of( List<RecommendStore> popularStores = List.of(
RecommendStore.builder() RecommendStore.builder()
.storeId(1L) .storeId(1L)
.storeName("인기 매장 1") .storeName("인기 매장 1")
.address("서울시 강남구") .address("서울시 강남구")
.category(category) .category(category)
.rating(4.5) .rating(4.5)
.reviewCount(100) .reviewCount(100)
.recommendScore(95.0) .recommendScore(95.0)
.recommendType(RecommendType.POPULARITY_BASED) .recommendType(RecommendType.POPULARITY_BASED)
.recommendReason("높은 평점과 많은 리뷰") .recommendReason("높은 평점과 많은 리뷰")
.build() .build()
); );
return popularStores.stream() return popularStores.stream()
.limit(limit != null ? limit : 10) .limit(limit != null ? limit : 10)
.map(this::toRecommendStoreResponse) .map(this::toRecommendStoreResponse)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/** /**
* 추천 결과 통합 점수 계산 * 추천 결과 통합 점수 계산
*/ */
private List<RecommendStore> combineRecommendations(List<RecommendStore> aiStores, private List<RecommendStore> combineRecommendations(List<RecommendStore> aiStores,
List<RecommendStore> locationStores, List<RecommendStore> locationStores,
TasteProfile profile) { TasteProfile profile) {
// AI 추천과 위치 기반 추천을 통합하여 최종 점수 계산 // AI 추천과 위치 기반 추천을 통합하여 최종 점수 계산
// 실제로는 복잡한 로직이 필요 // 실제로는 복잡한 로직이 필요
return aiStores.stream() return aiStores.stream()
.map(store -> store.updateRecommendScore( .map(store -> store.updateRecommendScore(
calculateFinalScore(store, profile) calculateFinalScore(store, profile)
)) ))
.sorted((s1, s2) -> Double.compare(s2.getRecommendScore(), s1.getRecommendScore())) .sorted((s1, s2) -> Double.compare(s2.getRecommendScore(), s1.getRecommendScore()))
.limit(20) .limit(20)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/** /**
* 최종 추천 점수 계산 * 최종 추천 점수 계산
*/ */
@ -135,25 +139,25 @@ public class StoreRecommendInteractor implements StoreRecommendUseCase {
double ratingScore = store.getRating() != null ? store.getRating() * 10 : 0.0; 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 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; 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); return (baseScore * 0.4) + (ratingScore * 0.3) + (reviewScore * 0.2) + (distanceScore * 0.1);
} }
/** /**
* 도메인을 응답 DTO로 변환 * 도메인을 응답 DTO로 변환
*/ */
private RecommendStoreResponse toRecommendStoreResponse(RecommendStore store) { private RecommendStoreResponse toRecommendStoreResponse(RecommendStore store) {
return RecommendStoreResponse.builder() return RecommendStoreResponse.builder()
.storeId(store.getStoreId()) .storeId(store.getStoreId())
.storeName(store.getStoreName()) .storeName(store.getStoreName())
.address(store.getAddress()) .address(store.getAddress())
.category(store.getCategory()) .category(store.getCategory())
.tags(store.getTags()) .tags(store.getTags())
.rating(store.getRating()) .rating(store.getRating())
.reviewCount(store.getReviewCount()) .reviewCount(store.getReviewCount())
.distance(store.getDistance()) .distance(store.getDistance())
.recommendScore(store.getRecommendScore()) .recommendScore(store.getRecommendScore())
.recommendReason(store.getRecommendReason()) .recommendReason(store.getRecommendReason())
.build(); .build();
} }
} }

View File

@ -52,4 +52,7 @@ public class RecommendStoreResponse {
@Schema(description = "태그 목록", example = "[\"매운맛\", \"혼밥\"]") @Schema(description = "태그 목록", example = "[\"매운맛\", \"혼밥\"]")
private List<String> tags; private List<String> tags;
@Schema(description = "가게 점수")
private Double rating;
} }

View File

@ -17,42 +17,66 @@ public class StoreDetailResponse {
@Schema(description = "매장 ID", example = "1") @Schema(description = "매장 ID", example = "1")
private Long storeId; private Long storeId;
@Schema(description = "매장명", example = "맛있는 김치찌개") @Schema(description = "매장명", example = "맛있는 한식당")
private String storeName; private String storeName;
@Schema(description = "카테고리", example = "한식")
private String category;
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123") @Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
private String address; private String address;
@Schema(description = "전화번호", example = "02-1234-5678") @Schema(description = "카테고리", example = "한식")
private String phoneNumber; private String category;
@Schema(description = "위도", example = "37.5665") @Schema(description = "평점", example = "4.5")
private Double latitude; private Double rating;
@Schema(description = "경도", example = "126.9780") @Schema(description = "리뷰 수", example = "256")
private Double longitude;
@Schema(description = "평균 평점", example = "4.5")
private Double averageRating;
@Schema(description = "리뷰 수", example = "127")
private Integer reviewCount; private Integer reviewCount;
@Schema(description = "거리(미터)", example = "500")
private Integer distance;
@Schema(description = "태그 목록", example = "[\"맛집\", \"혼밥\", \"가성비\"]")
private List<String> tags;
@Schema(description = "매장 이미지 URL 목록") @Schema(description = "매장 이미지 URL 목록")
private List<String> imageUrls; private List<String> images;
@Schema(description = "운영시간", example = "10:00-22:00") @Schema(description = "매장 설명")
private String operatingHours; private String description;
@Schema(description = "AI 요약", example = "친절한 서비스와 맛있는 김치찌개로 유명한 곳입니다") @Schema(description = "개인화된 추천 이유", example = "당신이 좋아하는 '매운맛' 특성을 가진 매장입니다")
private String personalizedReason;
@Schema(description = "AI 요약", example = "고객들이 매운맛과 친절한 서비스를 칭찬하는 매장입니다")
private String aiSummary; private String aiSummary;
@Schema(description = "긍정 키워드", example = "[\"맛있다\", \"친절하다\", \"깔끔하다\"]") @Schema(description = "운영 시간")
private List<String> topPositiveKeywords; private String operatingHours;
@Schema(description = "부정 키워드", example = "[\"시끄럽다\", \"대기시간\"]") @Schema(description = "전화번호")
private List<String> topNegativeKeywords; private String phoneNumber;
@Schema(description = "메뉴 정보")
private List<MenuInfo> 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;
}
} }

View File

@ -31,6 +31,12 @@ public class TasteAnalysisResponse {
@Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\"]") @Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\"]")
private List<String> preferredTags; private List<String> preferredTags;
@Schema(description = "가격 선호도 (0-100 점수)", example = "65.5")
private Double pricePreference;
@Schema(description = "거리 선호도 (0-100 점수)", example = "70.0")
private Double distancePreference;
@Schema(description = "분석 일시") @Schema(description = "분석 일시")
private LocalDateTime analysisDate; private LocalDateTime analysisDate;
@ -39,4 +45,5 @@ public class TasteAnalysisResponse {
@Schema(description = "추천사항", example = "[\"한식 카테고리의 새로운 매장을 시도해보세요\"]") @Schema(description = "추천사항", example = "[\"한식 카테고리의 새로운 매장을 시도해보세요\"]")
private List<String> recommendations; private List<String> recommendations;
} }