Fix : 수정

This commit is contained in:
lsh9672 2025-06-13 01:10:25 +09:00
parent a13ca2e4be
commit 60ee715d3e
11 changed files with 326 additions and 303 deletions

View File

@ -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();
}
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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;

View File

@ -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;

View File

@ -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;
} }

View File

@ -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,36 +19,21 @@ 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(List.of("맛집", "깔끔", "한식")) .tags(Arrays.asList("맛집", "깔끔", "한식"))
.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(),
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() .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(List.of("파스타", "분위기", "양식")) .tags(Arrays.asList("파스타", "분위기", "데이트"))
.rating(4.4) .rating(4.2)
.reviewCount(203) .reviewCount(78)
.distance(1200.0) .distance(1200.0)
.recommendScore(85.0) .recommendScore(85.0)
.recommendType(RecommendType.SIMILAR_USER) .recommendType(RecommendType.COLLABORATIVE_FILTERING)
.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가 분석한 매장 요약: 고객들이 맛과 서비스를 높이 평가하는 매장입니다.";
} }
} }

View File

@ -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,7 +20,6 @@ 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())
@ -36,17 +32,16 @@ public class LocationServiceAdapter implements LocationRepository {
} }
@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(List.of("근처", "맛집")) .tags(Arrays.asList("근처", "맛집"))
.rating(4.1) .rating(4.1)
.reviewCount(95) .reviewCount(95)
.distance(300.0) .distance(300.0)
@ -60,7 +55,7 @@ public class LocationServiceAdapter implements LocationRepository {
.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)
@ -73,27 +68,24 @@ public class LocationServiceAdapter implements LocationRepository {
@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)

View File

@ -1,35 +1,44 @@
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) {
try {
RecommendHistoryEntity entity = toRecommendHistoryEntity(history); RecommendHistoryEntity entity = toRecommendHistoryEntity(history);
RecommendHistoryEntity savedEntity = recommendHistoryJpaRepository.save(entity); RecommendHistoryEntity savedEntity = recommendHistoryJpaRepository.save(entity);
return toRecommendHistory(savedEntity); return toRecommendHistory(savedEntity);
} catch (Exception e) {
log.error("추천 히스토리 저장 실패", e);
throw new RuntimeException("추천 히스토리 저장 실패", e);
}
} }
@Override @Override
@ -37,14 +46,19 @@ public class RecommendRepositoryAdapter implements RecommendRepository {
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) {
try {
TasteProfileEntity entity = toTasteProfileEntity(profile); TasteProfileEntity entity = toTasteProfileEntity(profile);
TasteProfileEntity savedEntity = tasteProfileJpaRepository.save(entity); TasteProfileEntity savedEntity = tasteProfileJpaRepository.save(entity);
return toTasteProfile(savedEntity); return toTasteProfile(savedEntity);
} catch (Exception e) {
log.error("취향 프로필 저장 실패", e);
throw new RuntimeException("취향 프로필 저장 실패", e);
}
} }
@Override @Override
@ -53,15 +67,64 @@ public class RecommendRepositoryAdapter implements RecommendRepository {
.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();
@ -76,35 +139,40 @@ public class RecommendRepositoryAdapter implements RecommendRepository {
.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) {
try {
return RecommendHistoryEntity.builder() 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) {
try {
return TasteProfileEntity.builder() 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);
}
} }
} }

View File

@ -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,16 +29,15 @@ 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,
@ -56,16 +50,13 @@ public class UserPreferenceAdapter implements UserPreferenceRepository {
@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()
@ -75,7 +66,7 @@ public class UserPreferenceAdapter implements UserPreferenceRepository {
.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()) {