-
Notifications
You must be signed in to change notification settings - Fork 1
[feat] 웨이블존 추천 시스템 구현 완료 #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 27 commits
abb0031
e8b6b54
d4371a8
2f5c861
d63d0fd
b60ec90
d62c354
5154e55
d525040
5502ac1
9426867
b020978
86db2bd
10171cb
56f97db
43c3c05
b12229b
ecd2ae8
3befa38
d7efd3f
cea00c9
c6730cf
26b0fb4
3c336b9
8b8f5b7
e27cfa8
c8327dc
b23d589
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.wayble.server.explore.dto.recommend; | ||
|
|
||
| import jakarta.validation.constraints.DecimalMax; | ||
| import jakarta.validation.constraints.DecimalMin; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record WaybleZoneRecommendConditionDto( | ||
| @DecimalMin(value = "-90.0", message = "위도는 -90.0 이상이어야 합니다.") | ||
| @DecimalMax(value = "90.0", message = "위도는 90.0 이하여야 합니다.") | ||
| @NotNull(message = "위도 입력은 필수입니다.") | ||
| Double latitude, | ||
|
|
||
| @DecimalMin(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다.") | ||
| @DecimalMax(value = "180.0", message = "경도는 180.0 이하여야 합니다.") | ||
| @NotNull(message = "경도 입력은 필수입니다.") | ||
| Double longitude, | ||
|
|
||
| @NotNull(message = "유저 ID는 필수입니다.") | ||
| Long userId | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,47 @@ | ||
| package com.wayble.server.explore.dto.recommend; | ||
|
|
||
| import com.wayble.server.explore.entity.WaybleZoneDocument; | ||
| import com.wayble.server.wayblezone.entity.WaybleZoneType; | ||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record WaybleZoneRecommendResponseDto( | ||
|
|
||
| String username | ||
| Long zoneId, | ||
|
|
||
| String zoneName, | ||
|
|
||
| WaybleZoneType zoneType, | ||
|
|
||
| String thumbnailImageUrl, | ||
|
|
||
| Double latitude, | ||
|
|
||
| Double longitude, | ||
|
|
||
| Double averageRating, | ||
|
|
||
| Long reviewCount, | ||
|
|
||
| Double distanceScore, | ||
|
|
||
| Double similarityScore, | ||
|
|
||
| Double recencyScore, | ||
|
|
||
| Double totalScore | ||
|
|
||
| ) { | ||
| public static WaybleZoneRecommendResponseDto from(WaybleZoneDocument waybleZoneDocument) { | ||
| return WaybleZoneRecommendResponseDto.builder() | ||
| .zoneId(waybleZoneDocument.getZoneId()) | ||
| .zoneName(waybleZoneDocument.getZoneName()) | ||
| .zoneType(waybleZoneDocument.getZoneType()) | ||
| .thumbnailImageUrl(waybleZoneDocument.getThumbnailImageUrl()) | ||
| .averageRating(waybleZoneDocument.getAverageRating()) | ||
| .reviewCount(waybleZoneDocument.getReviewCount()) | ||
| .latitude(waybleZoneDocument.getAddress().getLocation().getLat()) | ||
| .longitude(waybleZoneDocument.getAddress().getLocation().getLon()) | ||
| .build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||||||||||||||||||||||||||
| package com.wayble.server.explore.entity; | ||||||||||||||||||||||||||||||
| import org.springframework.data.annotation.Id; | ||||||||||||||||||||||||||||||
| import lombok.*; | ||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.annotations.DateFormat; | ||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.annotations.Document; | ||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.annotations.Field; | ||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.annotations.FieldType; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import java.time.LocalDate; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @ToString | ||||||||||||||||||||||||||||||
| @Builder | ||||||||||||||||||||||||||||||
| @Getter | ||||||||||||||||||||||||||||||
| @AllArgsConstructor | ||||||||||||||||||||||||||||||
| @NoArgsConstructor | ||||||||||||||||||||||||||||||
| @Document(indexName = "recommend_log") | ||||||||||||||||||||||||||||||
| public class RecommendLogDocument { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Id | ||||||||||||||||||||||||||||||
| @Field(name = "id") | ||||||||||||||||||||||||||||||
| private String id; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private Long userId; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private Long zoneId; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Field(type = FieldType.Date, format = DateFormat.date) | ||||||||||||||||||||||||||||||
| private LocalDate recommendationDate; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private Long recommendCount; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public void updateRecommendLog(LocalDate recommendationDate, Long recommendCount) { | ||||||||||||||||||||||||||||||
| this.recommendationDate = recommendationDate; | ||||||||||||||||||||||||||||||
| this.recommendCount = recommendCount; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 업데이트 메서드에 검증 로직 추가 권장
다음과 같이 검증 로직을 추가하는 것을 권장합니다: public void updateRecommendLog(LocalDate recommendationDate, Long recommendCount) {
+ if (recommendationDate == null) {
+ throw new IllegalArgumentException("추천 날짜는 null일 수 없습니다.");
+ }
+ if (recommendCount == null || recommendCount < 0) {
+ throw new IllegalArgumentException("추천 횟수는 0 이상이어야 합니다.");
+ }
this.recommendationDate = recommendationDate;
this.recommendCount = recommendCount;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.wayble.server.explore.exception; | ||
|
|
||
| import com.wayble.server.common.exception.ErrorCase; | ||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum VisitLogErrorCase implements ErrorCase { | ||
|
|
||
| USER_NOT_EXIST(400, 7001, "해당하는 유저가 존재하지 않습니다."), | ||
| ZONE_NOT_EXIST(400, 7002, "해당하는 웨이블존이 존재하지 않습니다."); | ||
|
|
||
| private final Integer httpStatusCode; | ||
| private final Integer errorCode; | ||
| private final String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.wayble.server.explore.repository; | ||
|
|
||
| import com.wayble.server.explore.entity.RecommendLogDocument; | ||
| import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.Optional; | ||
|
|
||
| public interface RecommendLogDocumentRepository extends ElasticsearchRepository<RecommendLogDocument, String> { | ||
| Optional<RecommendLogDocument> findByUserIdAndZoneId(Long userId, Long zoneId); | ||
|
|
||
| Boolean existsByUserIdAndZoneId(Long userId, Long zoneId); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Optional<RecommendLogDocument> findByUserIdAndRecommendationDate(Long userId, LocalDate recommendationDate); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,18 +1,152 @@ | ||||||||||||||||||||||||||||||||
| package com.wayble.server.explore.repository.recommend; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import co.elastic.clients.elasticsearch._types.GeoLocation; | ||||||||||||||||||||||||||||||||
| import co.elastic.clients.elasticsearch._types.SortOrder; | ||||||||||||||||||||||||||||||||
| import co.elastic.clients.elasticsearch._types.query_dsl.Query; | ||||||||||||||||||||||||||||||||
| import com.wayble.server.explore.dto.recommend.WaybleZoneRecommendResponseDto; | ||||||||||||||||||||||||||||||||
| import com.wayble.server.explore.entity.RecommendLogDocument; | ||||||||||||||||||||||||||||||||
| import com.wayble.server.explore.entity.WaybleZoneDocument; | ||||||||||||||||||||||||||||||||
| import com.wayble.server.explore.entity.WaybleZoneVisitLogDocument; | ||||||||||||||||||||||||||||||||
| import com.wayble.server.user.entity.User; | ||||||||||||||||||||||||||||||||
| import com.wayble.server.user.entity.Gender; | ||||||||||||||||||||||||||||||||
| import com.wayble.server.explore.entity.AgeGroup; | ||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.client.elc.NativeQuery; | ||||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.core.ElasticsearchOperations; | ||||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.core.SearchHits; | ||||||||||||||||||||||||||||||||
| import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; | ||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Repository; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import java.time.LocalDate; | ||||||||||||||||||||||||||||||||
| import java.time.temporal.ChronoUnit; | ||||||||||||||||||||||||||||||||
| import java.util.*; | ||||||||||||||||||||||||||||||||
| import java.util.stream.Collectors; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Repository | ||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||
| public class WaybleZoneQueryRecommendRepository { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private final ElasticsearchOperations operations; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public WaybleZoneRecommendResponseDto searchPersonalWaybleZone(User user) { | ||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||
| private static final IndexCoordinates ZONE_INDEX = IndexCoordinates.of("wayble_zone"); | ||||||||||||||||||||||||||||||||
| private static final IndexCoordinates LOG_INDEX = IndexCoordinates.of("wayble_zone_visit_log"); | ||||||||||||||||||||||||||||||||
| private static final IndexCoordinates RECOMMEND_LOG_INDEX = IndexCoordinates.of("recommend_log"); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // === [가중치 설정] === // | ||||||||||||||||||||||||||||||||
| private static final double DISTANCE_WEIGHT = 0.55; // 거리 기반 점수 가중치 | ||||||||||||||||||||||||||||||||
| private static final double SIMILARITY_WEIGHT = 0.15; // 유사 사용자 방문 이력 기반 점수 가중치 | ||||||||||||||||||||||||||||||||
| private static final double RECENCY_WEIGHT = 0.3; // 최근 추천 내역 기반 감점 가중치 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private static final int MAX_DAY_DIFF = 30; // 추천 감점 최대 기준일 (30일 전까지 고려) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 하드코딩된 가중치와 상수를 설정 가능하도록 변경 가중치와 상수들이 하드코딩되어 있어 비즈니스 요구사항 변경 시 코드 수정이 필요합니다. 설정 파일로 외부화하는 것을 권장합니다. @Value("${wayble.recommendation.weight.distance:0.55}")
private double distanceWeight;
@Value("${wayble.recommendation.weight.similarity:0.15}")
private double similarityWeight;
@Value("${wayble.recommendation.weight.recency:0.3}")
private double recencyWeight;
@Value("${wayble.recommendation.max-day-diff:30}")
private int maxDayDiff;🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public List<WaybleZoneRecommendResponseDto> searchPersonalWaybleZones(User user, double latitude, double longitude, int size) { | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 입력 파라미터 유효성 검증 필요
public List<WaybleZoneRecommendResponseDto> searchPersonalWaybleZones(User user, double latitude, double longitude, int size) {
+ // 위도/경도 유효성 검증
+ if (latitude < -90 || latitude > 90) {
+ throw new IllegalArgumentException("Invalid latitude: " + latitude);
+ }
+ if (longitude < -180 || longitude > 180) {
+ throw new IllegalArgumentException("Invalid longitude: " + longitude);
+ }
+ if (size <= 0) {
+ throw new IllegalArgumentException("Size must be positive: " + size);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| AgeGroup userAgeGroup = AgeGroup.fromBirthDate(user.getBirthDate()); | ||||||||||||||||||||||||||||||||
| Gender userGender = user.getGender(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 사용자의 위치 기준으로 50km 반경 이내 장소들 조회 | ||||||||||||||||||||||||||||||||
| Query geoQuery = Query.of(q -> q | ||||||||||||||||||||||||||||||||
| .bool(b -> b | ||||||||||||||||||||||||||||||||
| .filter(f -> f.geoDistance(gd -> gd | ||||||||||||||||||||||||||||||||
| .field("address.location") | ||||||||||||||||||||||||||||||||
| .location(loc -> loc.latlon(ll -> ll.lat(latitude).lon(longitude))) | ||||||||||||||||||||||||||||||||
| .distance("50km"))) | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 거리 기준 정렬하여 최대 100개 결과 조회 | ||||||||||||||||||||||||||||||||
| NativeQuery nativeQuery = NativeQuery.builder() | ||||||||||||||||||||||||||||||||
| .withQuery(geoQuery) | ||||||||||||||||||||||||||||||||
| .withSort(s -> s.geoDistance(gds -> gds | ||||||||||||||||||||||||||||||||
| .field("address.location") | ||||||||||||||||||||||||||||||||
| .location(GeoLocation.of(gl -> gl.latlon(ll -> ll.lat(latitude).lon(longitude)))) | ||||||||||||||||||||||||||||||||
| .order(SortOrder.Asc))) | ||||||||||||||||||||||||||||||||
| .withMaxResults(100) | ||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| SearchHits<WaybleZoneDocument> zoneHits = operations.search(nativeQuery, WaybleZoneDocument.class, ZONE_INDEX); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 전체 방문 로그를 최대 10,000건까지 조회 | ||||||||||||||||||||||||||||||||
| NativeQuery logQuery = NativeQuery.builder() | ||||||||||||||||||||||||||||||||
| .withMaxResults(10000) | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 10,000건까지 조회해도 성능 상 문제 없었나요?!?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 따로 성능 문제는 없더라구요! 그런데 아마 수치는 조금 조정할 것 같아요! 최근 30일 이내 기록만 조회한다던가..! |
||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| SearchHits<WaybleZoneVisitLogDocument> logHits = operations.search(logQuery, WaybleZoneVisitLogDocument.class, LOG_INDEX); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 대량 데이터 조회로 인한 성능 문제 방문 로그를 최대 10,000건까지 조회하고 있어 성능 문제가 발생할 수 있습니다. 특히 방문 로그가 많아질수록 메모리 사용량과 처리 시간이 증가합니다. 다음과 같은 개선 방안을 고려하세요:
// 전체 방문 로그를 최대 10,000건까지 조회
NativeQuery logQuery = NativeQuery.builder()
+ .withQuery(Query.of(q -> q.range(r -> r
+ .field("visitDate")
+ .gte(JsonData.of(LocalDate.now().minusDays(30))))))
.withMaxResults(10000)
.build();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| // zoneId 별로 유사 사용자 방문 횟수 가중치 계산 | ||||||||||||||||||||||||||||||||
| Map<Long, Double> zoneVisitScoreMap = new HashMap<>(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for (var hit : logHits) { | ||||||||||||||||||||||||||||||||
| WaybleZoneVisitLogDocument log = hit.getContent(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| double weight = 0.0; | ||||||||||||||||||||||||||||||||
| boolean ageMatch = log.getAgeGroup() == userAgeGroup; | ||||||||||||||||||||||||||||||||
| boolean genderMatch = log.getGender() == userGender; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (ageMatch && genderMatch) { | ||||||||||||||||||||||||||||||||
| weight = 1.0; // 성별, 연령 둘 다 일치 | ||||||||||||||||||||||||||||||||
| } else if (ageMatch) { | ||||||||||||||||||||||||||||||||
| weight = 0.7; // 연령만 일치 | ||||||||||||||||||||||||||||||||
| } else if (genderMatch) { | ||||||||||||||||||||||||||||||||
| weight = 0.2; // 성별만 일치 | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| zoneVisitScoreMap.merge(log.getZoneId(), weight, Double::sum); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 최근 추천 날짜 조회 -> 가까울수록 감점 | ||||||||||||||||||||||||||||||||
| NativeQuery recommendLogQuery = NativeQuery.builder() | ||||||||||||||||||||||||||||||||
| .withQuery(Query.of(q -> q.term(t -> t.field("userId").value(user.getId())))) | ||||||||||||||||||||||||||||||||
| .withMaxResults(1000) | ||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| SearchHits<RecommendLogDocument> recommendHits = operations.search(recommendLogQuery, RecommendLogDocument.class, RECOMMEND_LOG_INDEX); | ||||||||||||||||||||||||||||||||
| Map<Long, LocalDate> recentRecommendDateMap = recommendHits.stream() | ||||||||||||||||||||||||||||||||
| .map(hit -> hit.getContent()) | ||||||||||||||||||||||||||||||||
| .collect(Collectors.toMap(RecommendLogDocument::getZoneId, RecommendLogDocument::getRecommendationDate)); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 각 장소마다 점수 계산 후 DTO로 변환 | ||||||||||||||||||||||||||||||||
| return zoneHits.stream() | ||||||||||||||||||||||||||||||||
| .map(hit -> { | ||||||||||||||||||||||||||||||||
| WaybleZoneDocument zone = hit.getContent(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 거리 점수 계산 (가까울수록 높음) | ||||||||||||||||||||||||||||||||
| double distanceScore = (1.0 / (1.0 + ((Double) hit.getSortValues().get(0) / 1000.0))) * DISTANCE_WEIGHT; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 유사도 점수 (비슷한 사용자 방문수 반영) | ||||||||||||||||||||||||||||||||
| double similarityScore = (zoneVisitScoreMap.getOrDefault(zone.getZoneId(), 0.0) / 10.0) * SIMILARITY_WEIGHT; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 최근 추천일 기반 감점 계산 | ||||||||||||||||||||||||||||||||
| double recencyScore = RECENCY_WEIGHT; | ||||||||||||||||||||||||||||||||
| LocalDate lastRecommendDate = recentRecommendDateMap.get(zone.getZoneId()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (lastRecommendDate != null) { | ||||||||||||||||||||||||||||||||
| long daysSince = ChronoUnit.DAYS.between(lastRecommendDate, LocalDate.now()); | ||||||||||||||||||||||||||||||||
| double factor = 1.0 - Math.min(daysSince, MAX_DAY_DIFF) / (double) MAX_DAY_DIFF; // 0~1 | ||||||||||||||||||||||||||||||||
| recencyScore = RECENCY_WEIGHT * (1.0 - factor); // days=0 -> 0점, days=30 -> full 점수 | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| double totalScore = distanceScore + similarityScore + recencyScore; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return WaybleZoneRecommendResponseDto.builder() | ||||||||||||||||||||||||||||||||
| .zoneId(zone.getZoneId()) | ||||||||||||||||||||||||||||||||
| .zoneName(zone.getZoneName()) | ||||||||||||||||||||||||||||||||
| .zoneType(zone.getZoneType()) | ||||||||||||||||||||||||||||||||
| .thumbnailImageUrl(zone.getThumbnailImageUrl()) | ||||||||||||||||||||||||||||||||
| .latitude(zone.getAddress().getLocation().getLat()) | ||||||||||||||||||||||||||||||||
| .longitude(zone.getAddress().getLocation().getLon()) | ||||||||||||||||||||||||||||||||
| .averageRating(zone.getAverageRating()) | ||||||||||||||||||||||||||||||||
| .reviewCount(zone.getReviewCount()) | ||||||||||||||||||||||||||||||||
| .distanceScore(distanceScore) | ||||||||||||||||||||||||||||||||
| .similarityScore(similarityScore) | ||||||||||||||||||||||||||||||||
| .recencyScore(recencyScore) | ||||||||||||||||||||||||||||||||
| .totalScore(totalScore) | ||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| .sorted(Comparator.comparingDouble(WaybleZoneRecommendResponseDto::totalScore).reversed()) // 점수 내림차순 정렬 | ||||||||||||||||||||||||||||||||
| .limit(size) // 상위 size 개수만 반환 | ||||||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.