Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
abb0031
[feat] User의 WaybleZone 상세 조회 기록을 Log로 저장하는 기능 구현
KiSeungMin Jul 14, 2025
e8b6b54
[feat] User가 WaybleZone 상세 조회 시 기록을 남길 수 있도록 임시 코드 구현
KiSeungMin Jul 14, 2025
d4371a8
[feat] WaybleZone 추천 기능 초안 구현
KiSeungMin Jul 14, 2025
2f5c861
[feat] WaybleZone 추천 기능 controller, service 로직 구현
KiSeungMin Jul 14, 2025
d63d0fd
[chore] User 관련 Merge Conflict 해결
KiSeungMin Jul 17, 2025
b60ec90
[chore] User 관련 Merge Conflict 해결
KiSeungMin Jul 17, 2025
d62c354
[feat] VisitLog의 id를 무작위 문자열로 설정
KiSeungMin Jul 14, 2025
5154e55
[feat] 추천 시스템 테스트 데이터 삽입 코드 완성
KiSeungMin Jul 14, 2025
d525040
[feat] 추천 시스템 초안 구현 완료(에러 없이 조회 성공)
KiSeungMin Jul 14, 2025
5502ac1
[feat] 추천 시스템에서 상위 N개의 웨이블존을 구하는 기능 구현
KiSeungMin Jul 14, 2025
9426867
[feat] 상위 N개의 웨이블존을 구하는 기능 테스트 코드 작성 완료
KiSeungMin Jul 14, 2025
b020978
[feat] 유사 사용자 기반 추천 가중치 계산 기능 구현 완료
KiSeungMin Jul 17, 2025
86db2bd
[feat] 추천 시스템이 여러 개의 가게를 반환할 수 있도록 개선
KiSeungMin Jul 17, 2025
10171cb
[feat] 추천 시스템 http 요청 body를 dto로 받도록 개선
KiSeungMin Jul 17, 2025
56f97db
[feat] 추천 기록 저장 엔티티 구현
KiSeungMin Jul 17, 2025
43c3c05
[feat] 추천 기록 로그 엔티티 저장, 수정 로직 구현 완료
KiSeungMin Jul 17, 2025
b12229b
[fix] 추천 날짜가 Long 타입으로 저장되던 문제 해결
KiSeungMin Jul 17, 2025
ecd2ae8
[feat] 웨이블존 추천 결과 캐싱 기능 구현
KiSeungMin Jul 17, 2025
3befa38
[feat] 웨이블존 추천 기록 저장 테스트 완료
KiSeungMin Jul 17, 2025
d7efd3f
[feat] 추천 테스트 코드 출력 양식 개선
KiSeungMin Jul 17, 2025
cea00c9
[fix] 일부 document의 repository id 타입 매핑 오류 해결
KiSeungMin Jul 17, 2025
c6730cf
[chore] 웨이블존 조회 개수 필드명 count -> size로 변경
KiSeungMin Jul 17, 2025
26b0fb4
[feat] 추천 시스템 구현 완료
KiSeungMin Jul 17, 2025
3c336b9
[feat] 추천 시스템 테스트 완료
KiSeungMin Jul 17, 2025
8b8f5b7
[chore] 불필요한 import 제거
KiSeungMin Jul 17, 2025
e27cfa8
[chore] Elastic Search Query 연산 코드에 주석 추가
KiSeungMin Jul 17, 2025
c8327dc
[refactor] test용 임시 UserRegisterDto 삭제 -> createUser()로 대체
KiSeungMin Jul 17, 2025
b23d589
[feat] CodeRabbit이 알려준 교정사항 반영
KiSeungMin Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

@Configuration
public class ElasticsearchConfig extends ElasticsearchConfiguration {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.wayble.server.explore.controller;

import com.wayble.server.common.response.CommonResponse;
import com.wayble.server.explore.dto.recommend.WaybleZoneRecommendConditionDto;
import com.wayble.server.explore.dto.recommend.WaybleZoneRecommendResponseDto;
import com.wayble.server.explore.service.WaybleZoneRecommendService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
Expand All @@ -18,10 +19,17 @@ public class WaybleZoneRecommendController {

private final WaybleZoneRecommendService waybleZoneRecommendService;

@GetMapping("/{userId}")
public CommonResponse<WaybleZoneRecommendResponseDto> getWaybleZonePersonalRecommend(
@PathVariable("userId") Long userId) {
WaybleZoneRecommendResponseDto dto = waybleZoneRecommendService.getWaybleZonePersonalRecommend(userId);
return CommonResponse.success(dto);
@GetMapping()
public CommonResponse<List<WaybleZoneRecommendResponseDto>> getWaybleZonePersonalRecommend(
@Valid @ModelAttribute WaybleZoneRecommendConditionDto conditionDto,
@RequestParam(name = "size", defaultValue = "1") int size) {

List<WaybleZoneRecommendResponseDto> result = waybleZoneRecommendService.getWaybleZonePersonalRecommend(
conditionDto.userId(),
conditionDto.latitude(),
conditionDto.longitude(),
size
);
return CommonResponse.success(result);
}
}
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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

업데이트 메서드에 검증 로직 추가 권장

updateRecommendLog 메서드에 입력값 검증이 없어 잘못된 데이터가 설정될 수 있습니다.

다음과 같이 검증 로직을 추가하는 것을 권장합니다:

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void updateRecommendLog(LocalDate recommendationDate, Long recommendCount) {
this.recommendationDate = recommendationDate;
this.recommendCount = recommendCount;
}
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;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/explore/entity/RecommendLogDocument.java
around lines 32 to 35, the updateRecommendLog method lacks input validation,
which may allow invalid data to be set. Add validation checks to ensure
recommendationDate is not null and recommendCount is not negative or null before
assigning them to the fields. If validation fails, throw an appropriate
exception to prevent invalid state updates.

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class WaybleZoneVisitLogDocument {

@Id
@Field(name = "id")
private Long logId;
private String logId;

private Long userId;

Expand All @@ -31,7 +31,7 @@ public class WaybleZoneVisitLogDocument {

public static WaybleZoneVisitLogDocument fromEntity(User user, Long zoneId) {
return WaybleZoneVisitLogDocument.builder()
.logId(UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE)
.logId(UUID.randomUUID().toString())
.userId(user.getId())
.zoneId(zoneId)
.gender(user.getGender())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
@RequiredArgsConstructor
public enum RecommendErrorCase implements ErrorCase {

INVALID_USER(400, 6001, "잘못된 유저 정보입니다.");
INVALID_USER(400, 6001, "잘못된 유저 정보입니다."),

RECOMMEND_LOG_NOT_EXIST(400, 6002, "해당하는 추천 기록이 존재하지 않습니다."),

WAYBLE_ZONE_NOT_EXIST(400, 6003, "추천 기록에 해당하는 웨이블존이 존재하지 않습니다.");

private final Integer httpStatusCode;
private final Integer errorCode;
Expand Down
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);

Optional<RecommendLogDocument> findByUserIdAndRecommendationDate(Long userId, LocalDate recommendationDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import com.wayble.server.explore.entity.WaybleZoneVisitLogDocument;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;

public interface WaybleZoneVisitLogDocumentRepository extends ElasticsearchRepository<WaybleZoneVisitLogDocument, Long>{

public interface WaybleZoneVisitLogDocumentRepository extends ElasticsearchRepository<WaybleZoneVisitLogDocument, String>{
List<WaybleZoneVisitLogDocument> findAll();
}

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
Copy link

Choose a reason for hiding this comment

The 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
In
src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java
around lines 36 to 41, the weight constants and max day difference are
hardcoded, making it difficult to update without code changes. Refactor these
static final fields to instance variables injected via @Value annotations from
configuration properties, providing default values as shown. Remove the static
modifiers and initialize these variables using Spring's @Value to externalize
configuration for easier maintenance.


public List<WaybleZoneRecommendResponseDto> searchPersonalWaybleZones(User user, double latitude, double longitude, int size) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

입력 파라미터 유효성 검증 필요

latitudelongitude 파라미터의 유효성을 검증하지 않고 있습니다. 잘못된 좌표값이 입력될 경우 예상치 못한 결과가 발생할 수 있습니다.

 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public List<WaybleZoneRecommendResponseDto> searchPersonalWaybleZones(User user, double latitude, double longitude, int size) {
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);
}
// ... existing implementation ...
}
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java
at line 43, the method searchPersonalWaybleZones lacks validation for the
latitude and longitude parameters. Add checks to ensure latitude is between -90
and 90 and longitude is between -180 and 180. If the values are out of range,
throw an appropriate exception or handle the error to prevent unexpected
behavior.


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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10,000건까지 조회해도 성능 상 문제 없었나요?!?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

대량 데이터 조회로 인한 성능 문제

방문 로그를 최대 10,000건까지 조회하고 있어 성능 문제가 발생할 수 있습니다. 특히 방문 로그가 많아질수록 메모리 사용량과 처리 시간이 증가합니다.

다음과 같은 개선 방안을 고려하세요:

  1. 집계 쿼리를 사용하여 Elasticsearch에서 직접 계산
  2. 최근 N일 간의 로그만 조회
  3. 페이징 처리
 // 전체 방문 로그를 최대 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 전체 방문 로그를 최대 10,000건까지 조회
NativeQuery logQuery = NativeQuery.builder()
.withMaxResults(10000)
.build();
SearchHits<WaybleZoneVisitLogDocument> logHits = operations.search(logQuery, WaybleZoneVisitLogDocument.class, LOG_INDEX);
// 전체 방문 로그를 최대 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();
SearchHits<WaybleZoneVisitLogDocument> logHits = operations.search(logQuery, WaybleZoneVisitLogDocument.class, LOG_INDEX);
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java
around lines 70 to 76, the code fetches up to 10,000 visit logs at once, causing
potential performance and memory issues. To fix this, modify the query to either
aggregate data directly in Elasticsearch instead of retrieving all logs, limit
the query to only recent logs within a specific timeframe, or implement
pagination to fetch logs in smaller batches rather than all at once.

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