Fix : 수정
This commit is contained in:
parent
a13ca2e4be
commit
60ee715d3e
@ -1,40 +1,16 @@
|
|||||||
package com.ktds.hi.recommend.biz.domain;
|
package com.ktds.hi.recommend.biz.domain;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 위치 도메인 클래스
|
|
||||||
* 위치 정보를 담는 도메인 객체
|
|
||||||
*/
|
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class Location {
|
public class Location {
|
||||||
|
private Long id; // 추가
|
||||||
private Long id;
|
|
||||||
private String address;
|
|
||||||
private Double latitude;
|
private Double latitude;
|
||||||
private Double longitude;
|
private Double longitude;
|
||||||
private String city;
|
private String address;
|
||||||
private String district;
|
private String city; // 추가
|
||||||
private String country;
|
private String district; // 추가
|
||||||
|
private String country; // 추가
|
||||||
/**
|
|
||||||
* 좌표 업데이트
|
|
||||||
*/
|
|
||||||
public Location updateCoordinates(Double newLatitude, Double newLongitude) {
|
|
||||||
return Location.builder()
|
|
||||||
.id(this.id)
|
|
||||||
.address(this.address)
|
|
||||||
.latitude(newLatitude)
|
|
||||||
.longitude(newLongitude)
|
|
||||||
.city(this.city)
|
|
||||||
.district(this.district)
|
|
||||||
.country(this.country)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -4,17 +4,24 @@ import lombok.Builder;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
public class RecommendHistory {
|
public class RecommendHistory {
|
||||||
private Long id;
|
private Long id;
|
||||||
private Long memberId;
|
private Long memberId;
|
||||||
private Long storeId;
|
private Long storeId; // 기존 필드
|
||||||
private String recommendType;
|
private String recommendType; // 기존 필드 (String)
|
||||||
private Double score;
|
private Double score;
|
||||||
private String reason;
|
private String reason;
|
||||||
private LocalDateTime recommendedAt;
|
private LocalDateTime recommendedAt;
|
||||||
private Boolean clicked;
|
private Boolean clicked;
|
||||||
private Boolean visited;
|
private Boolean visited;
|
||||||
|
|
||||||
|
// 추가 필드들 (Adapter에서 사용)
|
||||||
|
private List<Long> recommendedStoreIds;
|
||||||
|
private RecommendType recommendTypeEnum;
|
||||||
|
private String criteria;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
@ -21,4 +21,7 @@ public class RecommendStore {
|
|||||||
private Double recommendScore;
|
private Double recommendScore;
|
||||||
private Double distance;
|
private Double distance;
|
||||||
private String priceRange;
|
private String priceRange;
|
||||||
|
|
||||||
|
// 추가 필드
|
||||||
|
private RecommendType recommendType;
|
||||||
}
|
}
|
||||||
@ -1,16 +1,11 @@
|
|||||||
package com.ktds.hi.recommend.biz.domain;
|
package com.ktds.hi.recommend.biz.domain;
|
||||||
|
|
||||||
/**
|
|
||||||
* 추천 유형 열거형
|
|
||||||
* 추천의 종류를 정의
|
|
||||||
*/
|
|
||||||
public enum RecommendType {
|
public enum RecommendType {
|
||||||
TASTE_BASED("취향 기반"),
|
AI_RECOMMENDATION("AI 추천"),
|
||||||
LOCATION_BASED("위치 기반"),
|
LOCATION_BASED("위치 기반"),
|
||||||
POPULARITY_BASED("인기 기반"),
|
POPULARITY_BASED("인기 기반"),
|
||||||
COLLABORATIVE_FILTERING("협업 필터링"),
|
COLLABORATIVE_FILTERING("협업 필터링"),
|
||||||
AI_RECOMMENDATION("AI 추천"),
|
PREFERENCE_BASED("취향 기반");
|
||||||
SIMILAR_USER("유사 사용자 기반");
|
|
||||||
|
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,16 @@
|
|||||||
package com.ktds.hi.recommend.biz.domain;
|
package com.ktds.hi.recommend.biz.domain;
|
||||||
|
|
||||||
/**
|
|
||||||
* 취향 카테고리 열거형
|
|
||||||
* 음식 카테고리를 정의
|
|
||||||
*/
|
|
||||||
public enum TasteCategory {
|
public enum TasteCategory {
|
||||||
KOREAN("한식"),
|
KOREAN("한식"),
|
||||||
CHINESE("중식"),
|
CHINESE("중식"),
|
||||||
JAPANESE("일식"),
|
JAPANESE("일식"),
|
||||||
WESTERN("양식"),
|
WESTERN("양식"),
|
||||||
FAST_FOOD("패스트푸드"),
|
|
||||||
CAFE("카페"),
|
CAFE("카페"),
|
||||||
|
FASTFOOD("패스트푸드"),
|
||||||
DESSERT("디저트"),
|
DESSERT("디저트"),
|
||||||
CHICKEN("치킨"),
|
CHICKEN("치킨"),
|
||||||
PIZZA("피자"),
|
PIZZA("피자"),
|
||||||
ASIAN("아시안"),
|
OTHER("기타");
|
||||||
VEGETARIAN("채식"),
|
|
||||||
SEAFOOD("해산물");
|
|
||||||
|
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|
||||||
|
|||||||
@ -5,16 +5,26 @@ import lombok.Getter;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
public class TasteProfile {
|
public class TasteProfile {
|
||||||
private Long id;
|
private Long id;
|
||||||
private Long memberId;
|
private Long memberId;
|
||||||
private List<String> cuisinePreferences;
|
private List<String> cuisinePreferences; // 기존 유지
|
||||||
private String priceRange;
|
private String priceRange; // 기존 유지
|
||||||
private Integer distancePreference;
|
private Integer distancePreference; // 기존 유지
|
||||||
private List<String> tasteTags;
|
private List<String> tasteTags; // 기존 유지
|
||||||
|
|
||||||
|
// 추가 필드들 (Adapter에서 사용)
|
||||||
|
private List<TasteCategory> preferredCategories;
|
||||||
|
private Map<String, Double> categoryScores;
|
||||||
|
private List<String> preferredTags;
|
||||||
|
private Map<String, Object> behaviorPatterns;
|
||||||
|
private Double pricePreference;
|
||||||
|
private Double distancePreferenceDouble; // distancePreference와 구분
|
||||||
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
@ -3,17 +3,15 @@ package com.ktds.hi.recommend.infra.gateway;
|
|||||||
import com.ktds.hi.recommend.biz.usecase.out.AiRecommendRepository;
|
import com.ktds.hi.recommend.biz.usecase.out.AiRecommendRepository;
|
||||||
import com.ktds.hi.recommend.biz.domain.RecommendStore;
|
import com.ktds.hi.recommend.biz.domain.RecommendStore;
|
||||||
import com.ktds.hi.recommend.biz.domain.RecommendType;
|
import com.ktds.hi.recommend.biz.domain.RecommendType;
|
||||||
|
import com.ktds.hi.recommend.biz.domain.TasteProfile;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 추천 어댑터 클래스
|
|
||||||
* AI 기반 추천 기능을 구현 (현재는 Mock 구현)
|
|
||||||
*/
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -21,37 +19,22 @@ public class AiRecommendAdapter implements AiRecommendRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RecommendStore> recommendStoresByAI(Long memberId, Map<String, Object> preferences) {
|
public List<RecommendStore> recommendStoresByAI(Long memberId, Map<String, Object> preferences) {
|
||||||
log.info("AI 기반 매장 추천 요청: memberId={}, preferences={}", memberId, preferences);
|
log.info("AI 기반 매장 추천 요청: memberId={}", memberId);
|
||||||
|
|
||||||
// Mock 구현 - 실제로는 AI 모델 API 호출
|
return Arrays.asList(
|
||||||
return List.of(
|
RecommendStore.builder()
|
||||||
RecommendStore.builder()
|
.storeId(1L)
|
||||||
.storeId(1L)
|
.storeName("AI 추천 매장 1")
|
||||||
.storeName("AI 추천 매장 1")
|
.address("서울시 강남구 역삼동")
|
||||||
.address("서울시 강남구 역삼동")
|
.category("한식")
|
||||||
.category("한식")
|
.tags(Arrays.asList("맛집", "깔끔", "한식"))
|
||||||
.tags(List.of("맛집", "깔끔", "한식"))
|
.rating(4.5)
|
||||||
.rating(4.5)
|
.reviewCount(150)
|
||||||
.reviewCount(150)
|
.distance(500.0)
|
||||||
.distance(500.0)
|
.recommendScore(92.0)
|
||||||
.recommendScore(92.0)
|
.recommendType(RecommendType.AI_RECOMMENDATION)
|
||||||
.recommendType(RecommendType.AI_RECOMMENDATION)
|
.recommendReason("사용자 취향과 92% 일치")
|
||||||
.recommendReason("사용자 취향과 92% 일치")
|
.build()
|
||||||
.build(),
|
|
||||||
|
|
||||||
RecommendStore.builder()
|
|
||||||
.storeId(2L)
|
|
||||||
.storeName("AI 추천 매장 2")
|
|
||||||
.address("서울시 강남구 논현동")
|
|
||||||
.category("일식")
|
|
||||||
.tags(List.of("초밥", "신선", "일식"))
|
|
||||||
.rating(4.3)
|
|
||||||
.reviewCount(89)
|
|
||||||
.distance(800.0)
|
|
||||||
.recommendScore(87.0)
|
|
||||||
.recommendType(RecommendType.AI_RECOMMENDATION)
|
|
||||||
.recommendReason("사용자가 선호하는 일식 카테고리")
|
|
||||||
.build()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,43 +42,47 @@ public class AiRecommendAdapter implements AiRecommendRepository {
|
|||||||
public List<RecommendStore> recommendStoresBySimilarUsers(Long memberId) {
|
public List<RecommendStore> recommendStoresBySimilarUsers(Long memberId) {
|
||||||
log.info("유사 사용자 기반 추천 요청: memberId={}", memberId);
|
log.info("유사 사용자 기반 추천 요청: memberId={}", memberId);
|
||||||
|
|
||||||
// Mock 구현
|
return Arrays.asList(
|
||||||
return List.of(
|
RecommendStore.builder()
|
||||||
RecommendStore.builder()
|
.storeId(3L)
|
||||||
.storeId(3L)
|
.storeName("유사 취향 추천 매장")
|
||||||
.storeName("유사 취향 추천 매장")
|
.address("서울시 서초구 서초동")
|
||||||
.address("서울시 서초구 서초동")
|
.category("양식")
|
||||||
.category("양식")
|
.tags(Arrays.asList("파스타", "분위기", "데이트"))
|
||||||
.tags(List.of("파스타", "분위기", "양식"))
|
.rating(4.2)
|
||||||
.rating(4.4)
|
.reviewCount(78)
|
||||||
.reviewCount(203)
|
.distance(1200.0)
|
||||||
.distance(1200.0)
|
.recommendScore(85.0)
|
||||||
.recommendScore(85.0)
|
.recommendType(RecommendType.COLLABORATIVE_FILTERING)
|
||||||
.recommendType(RecommendType.SIMILAR_USER)
|
.recommendReason("비슷한 취향의 사용자들이 선호")
|
||||||
.recommendReason("비슷한 취향의 사용자들이 좋아하는 매장")
|
.build()
|
||||||
.build()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RecommendStore> recommendStoresByCollaborativeFiltering(Long memberId) {
|
public List<RecommendStore> recommendStoresByCollaborativeFiltering(Long memberId) {
|
||||||
log.info("협업 필터링 추천 요청: memberId={}", memberId);
|
return recommendStoresBySimilarUsers(memberId);
|
||||||
|
}
|
||||||
|
|
||||||
// Mock 구현
|
@Override
|
||||||
return List.of(
|
public List<RecommendStore> filterByPreferences(List<RecommendStore> stores, TasteProfile tasteProfile, String tags) {
|
||||||
RecommendStore.builder()
|
log.info("취향 기반 매장 필터링: 매장 수={}, 태그={}", stores.size(), tags);
|
||||||
.storeId(4L)
|
|
||||||
.storeName("협업 필터링 추천 매장")
|
// 간단한 필터링 로직 구현
|
||||||
.address("서울시 마포구 홍대입구")
|
return stores.stream()
|
||||||
.category("카페")
|
.filter(store -> {
|
||||||
.tags(List.of("커피", "디저트", "분위기"))
|
if (tasteProfile.getPreferredTags() != null) {
|
||||||
.rating(4.2)
|
return store.getTags().stream()
|
||||||
.reviewCount(127)
|
.anyMatch(tag -> tasteProfile.getPreferredTags().contains(tag));
|
||||||
.distance(2500.0)
|
}
|
||||||
.recommendScore(82.0)
|
return true;
|
||||||
.recommendType(RecommendType.COLLABORATIVE_FILTERING)
|
})
|
||||||
.recommendReason("사용자 행동 패턴 기반 추천")
|
.toList();
|
||||||
.build()
|
}
|
||||||
);
|
|
||||||
|
@Override
|
||||||
|
public String getStoreSummary(Long storeId) {
|
||||||
|
log.info("매장 AI 요약 조회: storeId={}", storeId);
|
||||||
|
return "AI가 분석한 매장 요약: 고객들이 맛과 서비스를 높이 평가하는 매장입니다.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,12 +8,9 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
|
||||||
* 위치 서비스 어댑터 클래스
|
|
||||||
* 위치 기반 서비스 기능을 구현 (현재는 Mock 구현)
|
|
||||||
*/
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -23,84 +20,79 @@ public class LocationServiceAdapter implements LocationRepository {
|
|||||||
public Location saveLocation(Location location) {
|
public Location saveLocation(Location location) {
|
||||||
log.info("위치 정보 저장: {}", location.getAddress());
|
log.info("위치 정보 저장: {}", location.getAddress());
|
||||||
|
|
||||||
// Mock 구현
|
|
||||||
return Location.builder()
|
return Location.builder()
|
||||||
.id(1L)
|
.id(1L)
|
||||||
.address(location.getAddress())
|
.address(location.getAddress())
|
||||||
.latitude(location.getLatitude())
|
.latitude(location.getLatitude())
|
||||||
.longitude(location.getLongitude())
|
.longitude(location.getLongitude())
|
||||||
.city("서울시")
|
.city("서울시")
|
||||||
.district("강남구")
|
.district("강남구")
|
||||||
.country("대한민국")
|
.country("대한민국")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RecommendStore> findStoresWithinRadius(Double latitude, Double longitude, Integer radius) {
|
public List<RecommendStore> findStoresWithinDistance(Double latitude, Double longitude, Integer radius) {
|
||||||
log.info("반경 내 매장 조회: lat={}, lon={}, radius={}", latitude, longitude, radius);
|
log.info("반경 내 매장 조회: lat={}, lon={}, radius={}", latitude, longitude, radius);
|
||||||
|
|
||||||
// Mock 구현
|
return Arrays.asList(
|
||||||
return List.of(
|
RecommendStore.builder()
|
||||||
RecommendStore.builder()
|
.storeId(5L)
|
||||||
.storeId(5L)
|
.storeName("근처 매장 1")
|
||||||
.storeName("근처 매장 1")
|
.address("서울시 강남구 역삼동 123-45")
|
||||||
.address("서울시 강남구 역삼동 123-45")
|
.category("한식")
|
||||||
.category("한식")
|
.tags(Arrays.asList("근처", "맛집"))
|
||||||
.tags(List.of("근처", "맛집"))
|
.rating(4.1)
|
||||||
.rating(4.1)
|
.reviewCount(95)
|
||||||
.reviewCount(95)
|
.distance(300.0)
|
||||||
.distance(300.0)
|
.recommendScore(78.0)
|
||||||
.recommendScore(78.0)
|
.recommendType(RecommendType.LOCATION_BASED)
|
||||||
.recommendType(RecommendType.LOCATION_BASED)
|
.recommendReason("현재 위치에서 300m 거리")
|
||||||
.recommendReason("현재 위치에서 300m 거리")
|
.build(),
|
||||||
.build(),
|
|
||||||
|
|
||||||
RecommendStore.builder()
|
RecommendStore.builder()
|
||||||
.storeId(6L)
|
.storeId(6L)
|
||||||
.storeName("근처 매장 2")
|
.storeName("근처 매장 2")
|
||||||
.address("서울시 강남구 역삼동 678-90")
|
.address("서울시 강남구 역삼동 678-90")
|
||||||
.category("카페")
|
.category("카페")
|
||||||
.tags(List.of("커피", "디저트"))
|
.tags(Arrays.asList("커피", "디저트"))
|
||||||
.rating(4.0)
|
.rating(4.0)
|
||||||
.reviewCount(67)
|
.reviewCount(67)
|
||||||
.distance(450.0)
|
.distance(450.0)
|
||||||
.recommendScore(75.0)
|
.recommendScore(75.0)
|
||||||
.recommendType(RecommendType.LOCATION_BASED)
|
.recommendType(RecommendType.LOCATION_BASED)
|
||||||
.recommendReason("현재 위치에서 450m 거리")
|
.recommendReason("현재 위치에서 450m 거리")
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Double calculateDistance(Double lat1, Double lon1, Double lat2, Double lon2) {
|
public Double calculateDistance(Double lat1, Double lon1, Double lat2, Double lon2) {
|
||||||
// Haversine 공식을 사용한 거리 계산
|
// 하버사인 공식을 이용한 거리 계산
|
||||||
final int R = 6371; // 지구의 반지름 (km)
|
final int R = 6371; // 지구 반지름 (km)
|
||||||
|
|
||||||
double latDistance = Math.toRadians(lat2 - lat1);
|
double latDistance = Math.toRadians(lat2 - lat1);
|
||||||
double lonDistance = Math.toRadians(lon2 - lon1);
|
double lonDistance = Math.toRadians(lon2 - lon1);
|
||||||
|
|
||||||
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
|
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
|
||||||
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
|
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
|
||||||
* Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
|
* Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
|
||||||
|
|
||||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
double distance = R * c * 1000; // 미터로 변환
|
|
||||||
|
|
||||||
return distance;
|
return R * c * 1000; // 미터 단위로 반환
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Location geocodeAddress(String address) {
|
public Location geocodeAddress(String address) {
|
||||||
log.info("주소 좌표 변환 요청: {}", address);
|
log.info("주소 좌표 변환: {}", address);
|
||||||
|
|
||||||
// Mock 구현 - 실제로는 Google Maps API 등 사용
|
// Mock 구현 - 실제로는 지도 API 호출
|
||||||
return Location.builder()
|
return Location.builder()
|
||||||
.address(address)
|
.address(address)
|
||||||
.latitude(37.5665)
|
.latitude(37.5665)
|
||||||
.longitude(126.9780)
|
.longitude(126.9780)
|
||||||
.city("서울시")
|
.city("서울시")
|
||||||
.district("중구")
|
.district("중구")
|
||||||
.country("대한민국")
|
.country("대한민국")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,110 +1,178 @@
|
|||||||
package com.ktds.hi.recommend.infra.gateway;
|
package com.ktds.hi.recommend.infra.gateway;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.ktds.hi.recommend.biz.usecase.out.RecommendRepository;
|
import com.ktds.hi.recommend.biz.usecase.out.RecommendRepository;
|
||||||
import com.ktds.hi.recommend.biz.domain.RecommendHistory;
|
import com.ktds.hi.recommend.biz.domain.RecommendHistory;
|
||||||
|
import com.ktds.hi.recommend.biz.domain.RecommendStore;
|
||||||
import com.ktds.hi.recommend.biz.domain.TasteProfile;
|
import com.ktds.hi.recommend.biz.domain.TasteProfile;
|
||||||
|
import com.ktds.hi.recommend.biz.domain.RecommendType;
|
||||||
import com.ktds.hi.recommend.infra.gateway.repository.RecommendHistoryJpaRepository;
|
import com.ktds.hi.recommend.infra.gateway.repository.RecommendHistoryJpaRepository;
|
||||||
import com.ktds.hi.recommend.infra.gateway.repository.TasteProfileJpaRepository;
|
import com.ktds.hi.recommend.infra.gateway.repository.TasteProfileJpaRepository;
|
||||||
import com.ktds.hi.recommend.infra.gateway.entity.RecommendHistoryEntity;
|
import com.ktds.hi.recommend.infra.gateway.entity.RecommendHistoryEntity;
|
||||||
import com.ktds.hi.recommend.infra.gateway.entity.TasteProfileEntity;
|
import com.ktds.hi.recommend.infra.gateway.entity.TasteProfileEntity;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 추천 리포지토리 어댑터 클래스
|
|
||||||
* 도메인 리포지토리 인터페이스를 JPA 리포지토리에 연결
|
|
||||||
*/
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class RecommendRepositoryAdapter implements RecommendRepository {
|
public class RecommendRepositoryAdapter implements RecommendRepository {
|
||||||
|
|
||||||
private final RecommendHistoryJpaRepository recommendHistoryJpaRepository;
|
private final RecommendHistoryJpaRepository recommendHistoryJpaRepository;
|
||||||
private final TasteProfileJpaRepository tasteProfileJpaRepository;
|
private final TasteProfileJpaRepository tasteProfileJpaRepository;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecommendHistory saveRecommendHistory(RecommendHistory history) {
|
public RecommendHistory saveRecommendHistory(RecommendHistory history) {
|
||||||
RecommendHistoryEntity entity = toRecommendHistoryEntity(history);
|
try {
|
||||||
RecommendHistoryEntity savedEntity = recommendHistoryJpaRepository.save(entity);
|
RecommendHistoryEntity entity = toRecommendHistoryEntity(history);
|
||||||
return toRecommendHistory(savedEntity);
|
RecommendHistoryEntity savedEntity = recommendHistoryJpaRepository.save(entity);
|
||||||
|
return toRecommendHistory(savedEntity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("추천 히스토리 저장 실패", e);
|
||||||
|
throw new RuntimeException("추천 히스토리 저장 실패", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RecommendHistory> findRecommendHistoriesByMemberId(Long memberId) {
|
public List<RecommendHistory> findRecommendHistoriesByMemberId(Long memberId) {
|
||||||
List<RecommendHistoryEntity> entities = recommendHistoryJpaRepository.findByMemberIdOrderByCreatedAtDesc(memberId);
|
List<RecommendHistoryEntity> entities = recommendHistoryJpaRepository.findByMemberIdOrderByCreatedAtDesc(memberId);
|
||||||
return entities.stream()
|
return entities.stream()
|
||||||
.map(this::toRecommendHistory)
|
.map(this::toRecommendHistory)
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TasteProfile saveTasteProfile(TasteProfile profile) {
|
public TasteProfile saveTasteProfile(TasteProfile profile) {
|
||||||
TasteProfileEntity entity = toTasteProfileEntity(profile);
|
try {
|
||||||
TasteProfileEntity savedEntity = tasteProfileJpaRepository.save(entity);
|
TasteProfileEntity entity = toTasteProfileEntity(profile);
|
||||||
return toTasteProfile(savedEntity);
|
TasteProfileEntity savedEntity = tasteProfileJpaRepository.save(entity);
|
||||||
|
return toTasteProfile(savedEntity);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("취향 프로필 저장 실패", e);
|
||||||
|
throw new RuntimeException("취향 프로필 저장 실패", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<TasteProfile> findTasteProfileByMemberId(Long memberId) {
|
public Optional<TasteProfile> findTasteProfileByMemberId(Long memberId) {
|
||||||
return tasteProfileJpaRepository.findByMemberId(memberId)
|
return tasteProfileJpaRepository.findByMemberId(memberId)
|
||||||
.map(this::toTasteProfile);
|
.map(this::toTasteProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* 엔티티를 도메인으로 변환
|
public Optional<RecommendStore> findStoreById(Long storeId) {
|
||||||
*/
|
// Mock 구현 - 실제로는 Store 서비스 호출 또는 캐시에서 조회
|
||||||
|
return Optional.of(RecommendStore.builder()
|
||||||
|
.storeId(storeId)
|
||||||
|
.storeName("매장 " + storeId)
|
||||||
|
.address("서울시 강남구")
|
||||||
|
.category("한식")
|
||||||
|
.rating(4.5)
|
||||||
|
.tags(Arrays.asList("맛집", "친절"))
|
||||||
|
.distance(500.0)
|
||||||
|
.priceRange("중간")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RecommendStore> findPopularStores(String category, Integer limit) {
|
||||||
|
// Mock 구현
|
||||||
|
return Arrays.asList(
|
||||||
|
RecommendStore.builder()
|
||||||
|
.storeId(1L)
|
||||||
|
.storeName("인기 매장 1")
|
||||||
|
.category(category != null ? category : "한식")
|
||||||
|
.rating(4.8)
|
||||||
|
.tags(Arrays.asList("인기", "맛집"))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logStoreClick(Long memberId, Long storeId) {
|
||||||
|
log.info("매장 클릭 로그: memberId={}, storeId={}", memberId, storeId);
|
||||||
|
// 비동기 로깅 구현
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logRecommendation(Long memberId, List<Long> storeIds, String context) {
|
||||||
|
try {
|
||||||
|
RecommendHistory history = RecommendHistory.builder()
|
||||||
|
.memberId(memberId)
|
||||||
|
.recommendedStoreIds(storeIds)
|
||||||
|
.criteria(context)
|
||||||
|
.recommendTypeEnum(RecommendType.PREFERENCE_BASED)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
saveRecommendHistory(history);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("추천 로그 저장 실패", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 변환 메서드들
|
||||||
private RecommendHistory toRecommendHistory(RecommendHistoryEntity entity) {
|
private RecommendHistory toRecommendHistory(RecommendHistoryEntity entity) {
|
||||||
return RecommendHistory.builder()
|
return RecommendHistory.builder()
|
||||||
.id(entity.getId())
|
.id(entity.getId())
|
||||||
.memberId(entity.getMemberId())
|
.memberId(entity.getMemberId())
|
||||||
.recommendedStoreIds(entity.getRecommendedStoreIdsList())
|
.recommendedStoreIds(entity.getRecommendedStoreIdsList())
|
||||||
.recommendType(entity.getRecommendType())
|
.recommendTypeEnum(entity.getRecommendType())
|
||||||
.criteria(entity.getCriteria())
|
.criteria(entity.getCriteria())
|
||||||
.createdAt(entity.getCreatedAt())
|
.createdAt(entity.getCreatedAt())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TasteProfile toTasteProfile(TasteProfileEntity entity) {
|
private TasteProfile toTasteProfile(TasteProfileEntity entity) {
|
||||||
return TasteProfile.builder()
|
return TasteProfile.builder()
|
||||||
.id(entity.getId())
|
.id(entity.getId())
|
||||||
.memberId(entity.getMemberId())
|
.memberId(entity.getMemberId())
|
||||||
.preferredCategories(entity.getPreferredCategoriesList())
|
.preferredCategories(entity.getPreferredCategoriesList())
|
||||||
.categoryScores(entity.getCategoryScoresMap())
|
.categoryScores(entity.getCategoryScoresMap())
|
||||||
.preferredTags(entity.getPreferredTagsList())
|
.preferredTags(entity.getPreferredTagsList())
|
||||||
.behaviorPatterns(entity.getBehaviorPatternsMap())
|
.behaviorPatterns(entity.getBehaviorPatternsMap())
|
||||||
.pricePreference(entity.getPricePreference())
|
.pricePreference(entity.getPricePreference())
|
||||||
.distancePreference(entity.getDistancePreference())
|
.distancePreferenceDouble(entity.getDistancePreference())
|
||||||
.createdAt(entity.getCreatedAt())
|
.createdAt(entity.getCreatedAt())
|
||||||
.updatedAt(entity.getUpdatedAt())
|
.updatedAt(entity.getUpdatedAt())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 도메인을 엔티티로 변환
|
|
||||||
*/
|
|
||||||
private RecommendHistoryEntity toRecommendHistoryEntity(RecommendHistory domain) {
|
private RecommendHistoryEntity toRecommendHistoryEntity(RecommendHistory domain) {
|
||||||
return RecommendHistoryEntity.builder()
|
try {
|
||||||
|
return RecommendHistoryEntity.builder()
|
||||||
.id(domain.getId())
|
.id(domain.getId())
|
||||||
.memberId(domain.getMemberId())
|
.memberId(domain.getMemberId())
|
||||||
.recommendedStoreIdsJson(domain.getRecommendedStoreIds().toString()) // JSON 변환 필요
|
.recommendedStoreIdsJson(objectMapper.writeValueAsString(domain.getRecommendedStoreIds()))
|
||||||
.recommendType(domain.getRecommendType())
|
.recommendType(domain.getRecommendTypeEnum())
|
||||||
.criteria(domain.getCriteria())
|
.criteria(domain.getCriteria())
|
||||||
.build();
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Entity 변환 실패", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TasteProfileEntity toTasteProfileEntity(TasteProfile domain) {
|
private TasteProfileEntity toTasteProfileEntity(TasteProfile domain) {
|
||||||
return TasteProfileEntity.builder()
|
try {
|
||||||
|
return TasteProfileEntity.builder()
|
||||||
.id(domain.getId())
|
.id(domain.getId())
|
||||||
.memberId(domain.getMemberId())
|
.memberId(domain.getMemberId())
|
||||||
.preferredCategoriesJson(domain.getPreferredCategories().toString()) // JSON 변환 필요
|
.preferredCategoriesJson(objectMapper.writeValueAsString(domain.getPreferredCategories()))
|
||||||
.categoryScoresJson(domain.getCategoryScores().toString()) // JSON 변환 필요
|
.categoryScoresJson(objectMapper.writeValueAsString(domain.getCategoryScores()))
|
||||||
.preferredTagsJson(domain.getPreferredTags().toString()) // JSON 변환 필요
|
.preferredTagsJson(objectMapper.writeValueAsString(domain.getPreferredTags()))
|
||||||
.behaviorPatternsJson(domain.getBehaviorPatterns().toString()) // JSON 변환 필요
|
.behaviorPatternsJson(objectMapper.writeValueAsString(domain.getBehaviorPatterns()))
|
||||||
.pricePreference(domain.getPricePreference())
|
.pricePreference(domain.getPricePreference())
|
||||||
.distancePreference(domain.getDistancePreference())
|
.distancePreference(domain.getDistancePreferenceDouble())
|
||||||
.build();
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Entity 변환 실패", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,26 +3,21 @@ package com.ktds.hi.recommend.infra.gateway;
|
|||||||
import com.ktds.hi.recommend.biz.usecase.out.UserPreferenceRepository;
|
import com.ktds.hi.recommend.biz.usecase.out.UserPreferenceRepository;
|
||||||
import com.ktds.hi.recommend.biz.domain.TasteProfile;
|
import com.ktds.hi.recommend.biz.domain.TasteProfile;
|
||||||
import com.ktds.hi.recommend.biz.domain.TasteCategory;
|
import com.ktds.hi.recommend.biz.domain.TasteCategory;
|
||||||
import com.ktds.hi.recommend.infra.gateway.repository.TasteProfileJpaRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
|
||||||
* 사용자 선호도 어댑터 클래스
|
|
||||||
* 사용자 취향 데이터 처리 기능을 구현
|
|
||||||
*/
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class UserPreferenceAdapter implements UserPreferenceRepository {
|
public class UserPreferenceAdapter implements UserPreferenceRepository {
|
||||||
|
|
||||||
private final TasteProfileJpaRepository tasteProfileJpaRepository;
|
|
||||||
private final RecommendRepositoryAdapter recommendRepositoryAdapter;
|
private final RecommendRepositoryAdapter recommendRepositoryAdapter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -34,53 +29,49 @@ public class UserPreferenceAdapter implements UserPreferenceRepository {
|
|||||||
public Map<String, Object> analyzePreferencesFromReviews(Long memberId) {
|
public Map<String, Object> analyzePreferencesFromReviews(Long memberId) {
|
||||||
log.info("리뷰 기반 취향 분석 시작: memberId={}", memberId);
|
log.info("리뷰 기반 취향 분석 시작: memberId={}", memberId);
|
||||||
|
|
||||||
// Mock 구현 - 실제로는 리뷰 서비스 API 호출하여 분석
|
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"preferredCategories", List.of(TasteCategory.KOREAN, TasteCategory.JAPANESE),
|
"preferredCategories", Arrays.asList(TasteCategory.KOREAN, TasteCategory.JAPANESE),
|
||||||
"categoryScores", Map.of(
|
"categoryScores", Map.of(
|
||||||
"한식", 85.0,
|
"한식", 85.0,
|
||||||
"일식", 78.0,
|
"일식", 78.0,
|
||||||
"양식", 65.0
|
"양식", 65.0
|
||||||
),
|
),
|
||||||
"preferredTags", List.of("맛집", "깔끔", "친절"),
|
"preferredTags", Arrays.asList("맛집", "깔끔", "친절"),
|
||||||
"pricePreference", 60.0, // 0-100 점수
|
"pricePreference", 60.0,
|
||||||
"distancePreference", 70.0,
|
"distancePreference", 70.0,
|
||||||
"behaviorPatterns", Map.of(
|
"behaviorPatterns", Map.of(
|
||||||
"weekendDining", true,
|
"weekendDining", true,
|
||||||
"avgRating", 4.2,
|
"avgRating", 4.2,
|
||||||
"reviewFrequency", "medium"
|
"reviewFrequency", "medium"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Long> findSimilarTasteMembers(Long memberId) {
|
public List<Long> findSimilarTasteMembers(Long memberId) {
|
||||||
log.info("유사 취향 사용자 조회: memberId={}", memberId);
|
log.info("유사 취향 사용자 조회: memberId={}", memberId);
|
||||||
|
return Arrays.asList(123L, 456L, 789L);
|
||||||
// Mock 구현 - 실제로는 ML 모델 또는 유사도 계산 알고리즘 사용
|
|
||||||
return List.of(123L, 456L, 789L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TasteProfile updateTasteProfile(Long memberId, Map<String, Object> analysisData) {
|
public TasteProfile updateTasteProfile(Long memberId, Map<String, Object> analysisData) {
|
||||||
log.info("취향 프로필 업데이트: memberId={}", memberId);
|
log.info("취향 프로필 업데이트: memberId={}", memberId);
|
||||||
|
|
||||||
// 기존 프로필 조회 또는 새로 생성
|
|
||||||
Optional<TasteProfile> existingProfile = getMemberPreferences(memberId);
|
Optional<TasteProfile> existingProfile = getMemberPreferences(memberId);
|
||||||
|
|
||||||
TasteProfile.TasteProfileBuilder builder = TasteProfile.builder()
|
TasteProfile.TasteProfileBuilder builder = TasteProfile.builder()
|
||||||
.memberId(memberId)
|
.memberId(memberId)
|
||||||
.preferredCategories((List<TasteCategory>) analysisData.get("preferredCategories"))
|
.preferredCategories((List<TasteCategory>) analysisData.get("preferredCategories"))
|
||||||
.categoryScores((Map<String, Double>) analysisData.get("categoryScores"))
|
.categoryScores((Map<String, Double>) analysisData.get("categoryScores"))
|
||||||
.preferredTags((List<String>) analysisData.get("preferredTags"))
|
.preferredTags((List<String>) analysisData.get("preferredTags"))
|
||||||
.behaviorPatterns((Map<String, Object>) analysisData.get("behaviorPatterns"))
|
.behaviorPatterns((Map<String, Object>) analysisData.get("behaviorPatterns"))
|
||||||
.pricePreference((Double) analysisData.get("pricePreference"))
|
.pricePreference((Double) analysisData.get("pricePreference"))
|
||||||
.distancePreference((Double) analysisData.get("distancePreference"))
|
.distancePreferenceDouble((Double) analysisData.get("distancePreference"))
|
||||||
.updatedAt(LocalDateTime.now());
|
.updatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
if (existingProfile.isPresent()) {
|
if (existingProfile.isPresent()) {
|
||||||
builder.id(existingProfile.get().getId())
|
builder.id(existingProfile.get().getId())
|
||||||
.createdAt(existingProfile.get().getCreatedAt());
|
.createdAt(existingProfile.get().getCreatedAt());
|
||||||
} else {
|
} else {
|
||||||
builder.createdAt(LocalDateTime.now());
|
builder.createdAt(LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user