From ff9d6dd323a94922e8ddb9b41c8d83b6ac635e43 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 21 Aug 2025 21:57:51 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?MySQL=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/common/WaybleZoneInfoResponseDto.java | 18 ++ .../WaybleZoneQuerySearchMysqlRepository.java | 264 ++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java diff --git a/src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java b/src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java index 389a8a80..d9229ce9 100644 --- a/src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/common/WaybleZoneInfoResponseDto.java @@ -1,6 +1,8 @@ package com.wayble.server.explore.dto.common; +import com.wayble.server.explore.entity.EsWaybleZoneFacility; import com.wayble.server.explore.entity.WaybleZoneDocument; +import com.wayble.server.wayblezone.entity.WaybleZone; import com.wayble.server.wayblezone.entity.WaybleZoneType; import lombok.Builder; @@ -31,4 +33,20 @@ public static WaybleZoneInfoResponseDto from(WaybleZoneDocument document) { .facility(FacilityResponseDto.from(document.getFacility())) .build(); } + + public static WaybleZoneInfoResponseDto fromEntity(WaybleZone waybleZone) { + return WaybleZoneInfoResponseDto.builder() + .zoneId(waybleZone.getId()) + .zoneName(waybleZone.getZoneName()) + .zoneType(waybleZone.getZoneType()) + .thumbnailImageUrl(waybleZone.getMainImageUrl()) + .address(waybleZone.getAddress().toFullAddress()) + .latitude(waybleZone.getAddress().getLatitude()) + .longitude(waybleZone.getAddress().getLongitude()) + .averageRating(waybleZone.getRating()) + .reviewCount(waybleZone.getReviewCount()) + .facility(waybleZone.getFacility() != null ? + FacilityResponseDto.from(EsWaybleZoneFacility.from(waybleZone.getFacility())) : null) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java new file mode 100644 index 00000000..d8d44869 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchMysqlRepository.java @@ -0,0 +1,264 @@ +package com.wayble.server.explore.repository.search; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.wayble.server.explore.dto.search.request.WaybleZoneSearchConditionDto; +import com.wayble.server.explore.dto.search.response.WaybleZoneSearchResponseDto; +import com.wayble.server.wayblezone.entity.WaybleZone; +import com.wayble.server.wayblezone.entity.WaybleZoneType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Set; + +import static com.wayble.server.wayblezone.entity.QWaybleZone.waybleZone; + +@Repository +@RequiredArgsConstructor +public class WaybleZoneQuerySearchMysqlRepository { + + private final JPAQueryFactory queryFactory; + + private static final int DISTRICT_SEARCH_SIZE = 3; + + /** + * 조건에 따른 웨이블존 검색 (MySQL 버전) + */ + public Slice searchWaybleZonesByCondition(WaybleZoneSearchConditionDto cond, Pageable pageable) { + + int fetchSize = pageable.getPageSize() + 1; + double radius = cond.radiusKm() != null ? cond.radiusKm() : 50.0; + + // Haversine 거리 계산 공식 + NumberExpression distanceExpression = calculateHaversineDistance( + cond.latitude(), + cond.longitude() + ); + + // 조건 빌더 + BooleanBuilder whereConditions = new BooleanBuilder(); + + // 거리 조건 (반경 내) + whereConditions.and(distanceExpression.loe(radius)); + + // 존 타입 조건 + if (cond.zoneType() != null) { + whereConditions.and(waybleZone.zoneType.eq(cond.zoneType())); + } + + // 존 이름 조건 (MySQL LIKE 사용) + if (cond.zoneName() != null && !cond.zoneName().isBlank()) { + whereConditions.and(waybleZone.zoneName.containsIgnoreCase(cond.zoneName())); + } + + // 쿼리 실행 + List zones = queryFactory + .selectFrom(waybleZone) + .leftJoin(waybleZone.facility).fetchJoin() + .where(whereConditions) + .orderBy(distanceExpression.asc()) // 거리순 정렬 + .offset((long) pageable.getPageNumber() * fetchSize) + .limit(fetchSize) + .fetch(); + + // DTO 변환 + List dtos = zones.stream() + .map(zone -> { + double distance = calculateHaversineDistanceJava( + cond.latitude(), cond.longitude(), + zone.getAddress().getLatitude(), zone.getAddress().getLongitude() + ); + return WaybleZoneSearchResponseDto.fromEntity(zone, distance); + }) + .toList(); + + // 다음 페이지 존재 여부 판단 + boolean hasNext = dtos.size() > pageable.getPageSize(); + if (hasNext) { + dtos = dtos.subList(0, pageable.getPageSize()); + } + + return new SliceImpl<>(dtos, pageable, hasNext); + } + + /** + * 30m 이내이고 이름이 유사한 WaybleZone 찾기 (MySQL 버전) + */ + public WaybleZoneSearchResponseDto findSimilarWaybleZone(WaybleZoneSearchConditionDto cond) { + if (cond.zoneName() == null || cond.zoneName().isBlank()) { + return null; + } + + // Step 1: 30m(0.03km) 이내 지리적 필터 + NumberExpression distanceExpression = calculateHaversineDistance( + cond.latitude(), + cond.longitude() + ); + + // Step 2: 거리 필터 + 텍스트 유사도 검색 + List candidates = queryFactory + .selectFrom(waybleZone) + .leftJoin(waybleZone.facility).fetchJoin() + .where(distanceExpression.loe(0.03)) // 30m = 0.03km + .orderBy(distanceExpression.asc()) + .limit(10) + .fetch(); + + // Step 3: 메모리에서 텍스트 유사도 검사 + return candidates.stream() + .filter(zone -> isTextSimilar(zone.getZoneName(), cond.zoneName())) + .findFirst() + .map(zone -> { + double distance = calculateHaversineDistanceJava( + cond.latitude(), cond.longitude(), + zone.getAddress().getLatitude(), zone.getAddress().getLongitude() + ); + return WaybleZoneSearchResponseDto.fromEntity(zone, distance); + }) + .orElse(null); + } + + /** + * Haversine 거리 계산 (QueryDSL Expression) + */ + private NumberExpression calculateHaversineDistance(double userLat, double userLon) { + // 지구 반지름 (km) + final double EARTH_RADIUS = 6371.0; + + return Expressions.numberTemplate(Double.class, + "{0} * 2 * ASIN(SQRT(" + + "POWER(SIN(RADIANS({1} - {2}) / 2), 2) + " + + "COS(RADIANS({2})) * COS(RADIANS({1})) * " + + "POWER(SIN(RADIANS({3} - {4}) / 2), 2)" + + "))", + EARTH_RADIUS, + waybleZone.address.latitude, + userLat, + waybleZone.address.longitude, + userLon + ); + } + + /** + * Haversine 거리 계산 (Java 구현) + */ + private double calculateHaversineDistanceJava(double lat1, double lon1, double lat2, double lon2) { + final double R = 6371; // 지구 반지름 (km) + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; + } + + /** + * 텍스트 유사도 검사 (메모리 기반) + */ + private boolean isTextSimilar(String zoneName, String searchName) { + if (zoneName == null || searchName == null) { + return false; + } + + String normalizedZone = normalize(zoneName); + String normalizedSearch = normalize(searchName); + + // 1. 완전 일치 + if (normalizedZone.equals(normalizedSearch)) { + return true; + } + + // 2. 포함 관계 (기존 wildcard와 유사) + if (normalizedZone.contains(normalizedSearch) || + normalizedSearch.contains(normalizedZone)) { + return true; + } + + // 3. 편집 거리 (기존 fuzzy와 유사) - 70% 이상 유사 + if (calculateLevenshteinSimilarity(normalizedZone, normalizedSearch) > 0.7) { + return true; + } + + // 4. 자카드 유사도 (토큰 기반, 기존 match와 유사) - 60% 이상 유사 + return calculateJaccardSimilarity(normalizedZone, normalizedSearch) > 0.6; + } + + /** + * 텍스트 정규화 (공백, 특수문자 제거) + */ + private String normalize(String text) { + return text.replaceAll("\\s+", "") // 공백 제거 + .replaceAll("[^가-힣a-zA-Z0-9]", "") // 특수문자 제거 + .toLowerCase(); + } + + /** + * 레벤슈타인 거리 기반 유사도 (0.0 ~ 1.0) + */ + private double calculateLevenshteinSimilarity(String s1, String s2) { + if (s1.isEmpty() || s2.isEmpty()) { + return 0.0; + } + + int distance = levenshteinDistance(s1, s2); + int maxLength = Math.max(s1.length(), s2.length()); + return 1.0 - (double) distance / maxLength; + } + + /** + * 레벤슈타인 거리 계산 + */ + private int levenshteinDistance(String s1, String s2) { + int[][] dp = new int[s1.length() + 1][s2.length() + 1]; + + for (int i = 0; i <= s1.length(); i++) { + dp[i][0] = i; + } + for (int j = 0; j <= s2.length(); j++) { + dp[0][j] = j; + } + + for (int i = 1; i <= s1.length(); i++) { + for (int j = 1; j <= s2.length(); j++) { + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]); + } + } + } + + return dp[s1.length()][s2.length()]; + } + + /** + * 자카드 유사도 (문자 집합 기반, 0.0 ~ 1.0) + */ + private double calculateJaccardSimilarity(String s1, String s2) { + if (s1.isEmpty() && s2.isEmpty()) { + return 1.0; + } + if (s1.isEmpty() || s2.isEmpty()) { + return 0.0; + } + + Set set1 = s1.chars().mapToObj(c -> (char) c).collect(java.util.stream.Collectors.toSet()); + Set set2 = s2.chars().mapToObj(c -> (char) c).collect(java.util.stream.Collectors.toSet()); + + Set intersection = new java.util.HashSet<>(set1); + intersection.retainAll(set2); + + Set union = new java.util.HashSet<>(set1); + union.addAll(set2); + + return (double) intersection.size() / union.size(); + } +} \ No newline at end of file From 3a4bec38097ad6511324abbba5c0295b16401361 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 21 Aug 2025 21:58:14 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=8B=9C=EC=84=A4=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20MySQL=EB=A1=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/common/FacilityResponseDto.java | 1 + .../explore/entity/WaybleFacilityMySQL.java | 27 +++++++++++++++++++ .../WaybleFacilityMySQLRepository.java | 7 +++++ 3 files changed, 35 insertions(+) create mode 100644 src/main/java/com/wayble/server/explore/entity/WaybleFacilityMySQL.java create mode 100644 src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityMySQLRepository.java diff --git a/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java index 81e91253..70ba1bf1 100644 --- a/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java @@ -1,6 +1,7 @@ package com.wayble.server.explore.dto.common; import com.wayble.server.explore.entity.EsWaybleZoneFacility; +import com.wayble.server.wayblezone.entity.WaybleZoneFacility; import lombok.Builder; @Builder diff --git a/src/main/java/com/wayble/server/explore/entity/WaybleFacilityMySQL.java b/src/main/java/com/wayble/server/explore/entity/WaybleFacilityMySQL.java new file mode 100644 index 00000000..6dd7fe41 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/entity/WaybleFacilityMySQL.java @@ -0,0 +1,27 @@ +package com.wayble.server.explore.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "wayble_facility") +public class WaybleFacilityMySQL { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "latitude", nullable = false) + private Double latitude; + + @Column(name = "longitude", nullable = false) + private Double longitude; + + @Enumerated(EnumType.STRING) + @Column(name = "facility_type", nullable = false) + private FacilityType facilityType; +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityMySQLRepository.java b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityMySQLRepository.java new file mode 100644 index 00000000..38e0beb1 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityMySQLRepository.java @@ -0,0 +1,7 @@ +package com.wayble.server.explore.repository.facility; + +import com.wayble.server.explore.entity.WaybleFacilityMySQL; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface WaybleFacilityMySQLRepository extends JpaRepository { +} From 1ae21ba3ebf79905e689326d3ad17041dd867d19 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 21 Aug 2025 21:58:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20MySQL?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facility/WaybleFacilityResponseDto.java | 9 + .../response/WaybleZoneSearchResponseDto.java | 9 + ...bleFacilityQuerySearchMysqlRepository.java | 78 +++++++ ...ybleZoneQueryRecommendMysqlRepository.java | 192 ++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java create mode 100644 src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java index 3bbd399d..9d2902d2 100644 --- a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java @@ -1,6 +1,7 @@ package com.wayble.server.explore.dto.facility; import com.wayble.server.explore.entity.FacilityType; +import com.wayble.server.explore.entity.WaybleFacilityMySQL; import com.wayble.server.explore.entity.WaybleFacilityDocument; import lombok.AccessLevel; import lombok.Builder; @@ -20,4 +21,12 @@ public static WaybleFacilityResponseDto from(WaybleFacilityDocument facilityDocu .facilityType(facilityDocument.getFacilityType()) .build(); } + + public static WaybleFacilityResponseDto fromEntity(WaybleFacilityMySQL waybleFacilityMySQL) { + return WaybleFacilityResponseDto.builder() + .latitude(waybleFacilityMySQL.getLatitude()) + .longitude(waybleFacilityMySQL.getLongitude()) + .facilityType(waybleFacilityMySQL.getFacilityType()) + .build(); + } } diff --git a/src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java b/src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java index dcfe6c59..49679548 100644 --- a/src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java @@ -2,6 +2,7 @@ import com.wayble.server.explore.dto.common.WaybleZoneInfoResponseDto; import com.wayble.server.explore.entity.WaybleZoneDocument; +import com.wayble.server.wayblezone.entity.WaybleZone; import lombok.AccessLevel; import lombok.Builder; @@ -19,4 +20,12 @@ public static WaybleZoneSearchResponseDto from(WaybleZoneDocument waybleZoneDocu .distance(distance) .build(); } + + public static WaybleZoneSearchResponseDto fromEntity(WaybleZone waybleZone, Double distance) { + + return WaybleZoneSearchResponseDto.builder() + .waybleZoneInfo(WaybleZoneInfoResponseDto.fromEntity(waybleZone)) + .distance(distance) + .build(); + } } diff --git a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java new file mode 100644 index 00000000..bf2774ff --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchMysqlRepository.java @@ -0,0 +1,78 @@ +package com.wayble.server.explore.repository.facility; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.wayble.server.explore.dto.facility.WaybleFacilityConditionDto; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.entity.WaybleFacilityMySQL; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.wayble.server.explore.entity.QWaybleFacilityMySQL.waybleFacilityMySQL; + +@Repository +@RequiredArgsConstructor +public class WaybleFacilityQuerySearchMysqlRepository { + + private final JPAQueryFactory queryFactory; + + private static final int LIMIT = 50; + + /** + * 위도, 경도, 시설 타입을 바탕으로 WaybleFacility를 거리순으로 N개 반환 (MySQL/QueryDSL) + */ + public List findNearbyFacilitiesByType( + WaybleFacilityConditionDto condition) { + + // Haversine 거리 계산식 (QueryDSL Expression) + NumberExpression distanceExpression = calculateHaversineDistance( + condition.latitude(), condition.longitude()); + + // 조건 빌더 + BooleanBuilder whereClause = new BooleanBuilder(); + + // 시설 타입 조건 추가 + if (condition.facilityType() != null) { + whereClause.and(waybleFacilityMySQL.facilityType.eq(condition.facilityType())); + } + + // 반경 10km 이내 필터링 + whereClause.and(distanceExpression.loe(10.0)); + + List facilities = queryFactory + .selectFrom(waybleFacilityMySQL) + .where(whereClause) + .orderBy(distanceExpression.asc()) + .limit(LIMIT) + .fetch(); + + return facilities.stream() + .map(WaybleFacilityResponseDto::fromEntity) + .toList(); + } + + /** + * Haversine 거리 계산 (QueryDSL Expression) + */ + private NumberExpression calculateHaversineDistance(double userLat, double userLon) { + // 지구 반지름 (km) + final double EARTH_RADIUS = 6371.0; + + return Expressions.numberTemplate(Double.class, + "{0} * 2 * ASIN(SQRT(" + + "POWER(SIN(RADIANS({1} - {2}) / 2), 2) + " + + "COS(RADIANS({2})) * COS(RADIANS({1})) * " + + "POWER(SIN(RADIANS({3} - {4}) / 2), 2)" + + "))", + EARTH_RADIUS, + waybleFacilityMySQL.latitude, + userLat, + waybleFacilityMySQL.longitude, + userLon + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java b/src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java new file mode 100644 index 00000000..abceaa2e --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendMysqlRepository.java @@ -0,0 +1,192 @@ +package com.wayble.server.explore.repository.recommend; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.wayble.server.explore.dto.common.FacilityResponseDto; +import com.wayble.server.explore.dto.common.WaybleZoneInfoResponseDto; +import com.wayble.server.explore.dto.recommend.WaybleZoneRecommendResponseDto; +import com.wayble.server.explore.entity.EsWaybleZoneFacility; +import com.wayble.server.user.entity.User; +import com.wayble.server.user.entity.Gender; +import com.wayble.server.common.entity.AgeGroup; +import com.wayble.server.wayblezone.entity.WaybleZone; +import com.wayble.server.wayblezone.entity.WaybleZoneVisitLog; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +import static com.wayble.server.wayblezone.entity.QWaybleZone.waybleZone; +import static com.wayble.server.wayblezone.entity.QWaybleZoneVisitLog.waybleZoneVisitLog; + +@Repository +@RequiredArgsConstructor +public class WaybleZoneQueryRecommendMysqlRepository { + + private final JPAQueryFactory queryFactory; + + // === [가중치 설정] === // + 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일 전까지 고려) + + public List searchPersonalWaybleZones(User user, double latitude, double longitude, int size) { + + AgeGroup userAgeGroup = AgeGroup.fromBirthDate(user.getBirthDate()); + Gender userGender = user.getGender(); + + // Step 1: 50km 반경 이내 장소들 조회 (MySQL Haversine 공식 사용) + NumberExpression distanceExpression = calculateHaversineDistance(latitude, longitude); + + List nearbyZones = queryFactory + .selectFrom(waybleZone) + .leftJoin(waybleZone.facility).fetchJoin() + .where(distanceExpression.loe(50.0)) // 50km 이내 + .orderBy(distanceExpression.asc()) + .limit(100) // 상위 100개만 가져와서 성능 최적화 + .fetch(); + + // Step 2: 최근 30일 이내 방문 로그 조회 (MySQL) + LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30); + + List visitLogs = queryFactory + .selectFrom(waybleZoneVisitLog) + .where(waybleZoneVisitLog.visitedAt.goe(thirtyDaysAgo)) + .limit(10000) + .fetch(); + + // Step 3: zoneId 별로 유사 사용자 방문 횟수 가중치 계산 + Map zoneVisitScoreMap = new HashMap<>(); + + for (WaybleZoneVisitLog log : visitLogs) { + 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); + } + + // Step 4: 최근 추천 날짜 조회 (MySQL - 실제 추천 로그 테이블이 있다면) + // 여기서는 간단히 빈 맵으로 처리 (실제로는 추천 로그 테이블에서 조회) + Map recentRecommendDateMap = new HashMap<>(); + + // 실제 구현시에는 아래와 같이 추천 로그 테이블에서 조회 + /* + List recommendLogs = queryFactory + .selectFrom(recommendLog) + .where(recommendLog.userId.eq(user.getId())) + .limit(1000) + .fetch(); + + Map recentRecommendDateMap = recommendLogs.stream() + .collect(Collectors.toMap( + RecommendLog::getZoneId, + RecommendLog::getRecommendationDate, + (existing, replacement) -> existing.isAfter(replacement) ? existing : replacement + )); + */ + + // Step 5: 각 장소마다 점수 계산 후 DTO로 변환 + return nearbyZones.stream() + .map(zone -> { + // 거리 계산 (Java로 정확한 계산) + double distanceKm = calculateHaversineDistanceJava( + latitude, longitude, + zone.getAddress().getLatitude(), zone.getAddress().getLongitude() + ); + + // 거리 점수 계산 (가까울수록 높음) + double distanceScore = (1.0 / (1.0 + distanceKm)) * DISTANCE_WEIGHT; + + // 유사도 점수 (비슷한 사용자 방문수 반영) + double similarityScore = (zoneVisitScoreMap.getOrDefault(zone.getId(), 0.0) / 10.0) * SIMILARITY_WEIGHT; + + // 최근 추천일 기반 감점 계산 + double recencyScore = RECENCY_WEIGHT; + LocalDate lastRecommendDate = recentRecommendDateMap.get(zone.getId()); + + 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; + + WaybleZoneInfoResponseDto waybleZoneInfo = WaybleZoneInfoResponseDto.builder() + .zoneId(zone.getId()) + .zoneName(zone.getZoneName()) + .zoneType(zone.getZoneType()) + .thumbnailImageUrl(zone.getMainImageUrl()) + .address(zone.getAddress().toFullAddress()) + .latitude(zone.getAddress().getLatitude()) + .longitude(zone.getAddress().getLongitude()) + .averageRating(zone.getRating()) + .reviewCount(zone.getReviewCount()) + .facility(zone.getFacility() != null ? + FacilityResponseDto.from(EsWaybleZoneFacility.from(zone.getFacility())) : null) + .build(); + + return WaybleZoneRecommendResponseDto.builder() + .waybleZoneInfo(waybleZoneInfo) + .distanceScore(distanceScore) + .similarityScore(similarityScore) + .recencyScore(recencyScore) + .totalScore(totalScore) + .build(); + }) + .sorted(Comparator.comparingDouble(WaybleZoneRecommendResponseDto::totalScore).reversed()) // 점수 내림차순 정렬 + .limit(size) // 상위 size 개수만 반환 + .toList(); + } + + /** + * Haversine 거리 계산 (QueryDSL Expression) + */ + private NumberExpression calculateHaversineDistance(double userLat, double userLon) { + // 지구 반지름 (km) + final double EARTH_RADIUS = 6371.0; + + return Expressions.numberTemplate(Double.class, + "{0} * 2 * ASIN(SQRT(" + + "POWER(SIN(RADIANS({1} - {2}) / 2), 2) + " + + "COS(RADIANS({2})) * COS(RADIANS({1})) * " + + "POWER(SIN(RADIANS({3} - {4}) / 2), 2)" + + "))", + EARTH_RADIUS, + waybleZone.address.latitude, + userLat, + waybleZone.address.longitude, + userLon + ); + } + + /** + * Haversine 거리 계산 (Java 구현) + */ + private double calculateHaversineDistanceJava(double lat1, double lon1, double lat2, double lon2) { + final double R = 6371; // 지구 반지름 (km) + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; + } +} \ No newline at end of file From b7959ff3f9f61eaffe8d3ea97b93c37c75219d6e Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Thu, 21 Aug 2025 21:59:56 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9D=98=20=EA=B0=81=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=8B=A4=ED=96=89=20=EC=8B=9C=EA=B0=84=EC=9D=84=20?= =?UTF-8?q?=EC=95=8C=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleFacilityApiIntegrationTest.java | 45 ++++++++++++++- ...WaybleZoneRecommendApiIntegrationTest.java | 36 ++++++++++-- .../WaybleZoneSearchApiIntegrationTest.java | 56 +++++++++++++------ 3 files changed, 112 insertions(+), 25 deletions(-) diff --git a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java index a12691d4..4bbe5cb6 100644 --- a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java @@ -7,7 +7,9 @@ import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; import com.wayble.server.explore.entity.FacilityType; import com.wayble.server.explore.entity.WaybleFacilityDocument; +import com.wayble.server.explore.entity.WaybleFacilityMySQL; import com.wayble.server.explore.repository.facility.WaybleFacilityDocumentRepository; +import com.wayble.server.explore.repository.facility.WaybleFacilityMySQLRepository; import com.wayble.server.user.entity.Gender; import com.wayble.server.user.entity.LoginType; import com.wayble.server.user.entity.User; @@ -50,6 +52,9 @@ public class WaybleFacilityApiIntegrationTest { @Autowired private UserRepository userRepository; + @Autowired + private WaybleFacilityMySQLRepository waybleFacilityMySQLRepository; + @Autowired private WaybleFacilityDocumentRepository waybleFacilityDocumentRepository; @@ -59,7 +64,7 @@ public class WaybleFacilityApiIntegrationTest { private static final double RADIUS = 20.0; - private static final int SAMPLES = 100; + private static final int SAMPLES = 10000; private static final String baseUrl = "/api/v1/facilities/search"; @@ -67,7 +72,7 @@ public class WaybleFacilityApiIntegrationTest { private String token; - @BeforeAll + @BeforeEach public void setup() { User testUser = User.createUserWithDetails( "testUser", "testUsername", UUID.randomUUID() + "@email", "password", @@ -95,12 +100,30 @@ public void setup() { waybleFacilityDocumentRepository.save(rampDocument); waybleFacilityDocumentRepository.save(elevatorDocument); + + WaybleFacilityMySQL ramp = WaybleFacilityMySQL. + builder() + .longitude(points.get("longitude")) + .latitude(points.get("latitude")) + .facilityType(FacilityType.RAMP) + .build(); + + WaybleFacilityMySQL elevator = WaybleFacilityMySQL. + builder() + .longitude(points.get("longitude")) + .latitude(points.get("latitude")) + .facilityType(FacilityType.ELEVATOR) + .build(); + + waybleFacilityMySQLRepository.save(ramp); + waybleFacilityMySQLRepository.save(elevator); } } - @AfterAll + @AfterEach public void teardown() { waybleFacilityDocumentRepository.deleteAll(); + waybleFacilityMySQLRepository.deleteAll(); userRepository.deleteById(userId); } @@ -121,6 +144,9 @@ public void checkDataExists() { @Test @DisplayName("좌표를 전달받아 가까운 경사로 조회 테스트") public void findNearbyRampFacilities() throws Exception { + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl) .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) @@ -130,11 +156,16 @@ public void findNearbyRampFacilities() throws Exception { ) .andExpect(status().is2xxSuccessful()) .andReturn(); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms (경사로 조회)"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -179,6 +210,9 @@ public void findNearbyRampFacilities() throws Exception { @Test @DisplayName("좌표를 전달받아 가까운 엘리베이터 조회 테스트") public void findNearbyElevatorFacilities() throws Exception { + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl) .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) @@ -188,11 +222,16 @@ public void findNearbyElevatorFacilities() throws Exception { ) .andExpect(status().is2xxSuccessful()) .andReturn(); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms (엘리베이터 조회)"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); diff --git a/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java index 41130d60..6bc8441d 100644 --- a/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java @@ -76,7 +76,7 @@ public class WaybleZoneRecommendApiIntegrationTest { private static final double RADIUS = 50.0; - private static final Long SAMPLES = 100L; + private static final Long SAMPLES = 10000L; private static final String baseUrl = "/api/v1/wayble-zones/recommend"; @@ -97,7 +97,7 @@ public class WaybleZoneRecommendApiIntegrationTest { "노브랜드버거" )); - @BeforeAll + @BeforeEach public void setup() { User testUser = User.createUserWithDetails( "testUser", "testUsername", UUID.randomUUID() + "@email", "password", @@ -108,7 +108,7 @@ public void setup() { userId = testUser.getId(); token = jwtTokenProvider.generateToken(userId, "ROLE_USER"); - for (int i = 1; i <= SAMPLES / 2; i++) { + for (int i = 1; i <= SAMPLES / 3; i++) { Long zoneId = (long) (Math.random() * SAMPLES) + 1; if(!recommendLogDocumentRepository.existsByUserIdAndZoneId(userId, zoneId)) { RecommendLogDocument recommendLogDocument = RecommendLogDocument @@ -154,12 +154,12 @@ public void setup() { waybleZoneDocumentRepository.save(waybleZoneDocument); User user = User.createUserWithDetails( - "user" + i, "username" + i, UUID.randomUUID() + "@email", "password", + "u" + i, "n" + i, UUID.randomUUID() + "@email", "password", generateRandomBirthDate(), Gender.values()[i % 2], LoginType.KAKAO, UserType.DISABLED ); userRepository.save(user); - int count = (int) (Math.random() * 30) + 1; + int count = (int) (Math.random() * 20) + 1; for (int j = 0; j < count; j++) { Long zoneId = (long) (Math.random() * SAMPLES) + 1; WaybleZoneVisitLog visitLogDocument = WaybleZoneVisitLog @@ -176,7 +176,7 @@ public void setup() { } } - @AfterAll + @AfterEach public void teardown() { waybleZoneDocumentRepository.deleteAll(); waybleZoneVisitLogRepository.deleteAll(); @@ -204,6 +204,9 @@ public void checkDataExists() { @Test @DisplayName("추천 기록 저장 테스트") public void saveRecommendLogTest() throws Exception { + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl) .header("Authorization", "Bearer " + token) .param("userId", String.valueOf(userId)) @@ -213,11 +216,16 @@ public void saveRecommendLogTest() throws Exception { ) .andExpect(status().is2xxSuccessful()) .andReturn(); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -241,6 +249,9 @@ public void saveRecommendLogTest() throws Exception { @Test @DisplayName("추천 기능 테스트") public void recommendWaybleZone() throws Exception { + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl) .header("Authorization", "Bearer " + token) .param("userId", String.valueOf(userId)) @@ -250,11 +261,16 @@ public void recommendWaybleZone() throws Exception { ) .andExpect(status().is2xxSuccessful()) .andReturn(); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -282,6 +298,9 @@ public void recommendWaybleZone() throws Exception { @Test @DisplayName("추천 결과 상위 N개 값 테스트") public void recommendWaybleZoneTop20() throws Exception { + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl) .header("Authorization", "Bearer " + token) .param("userId", String.valueOf(userId)) @@ -292,11 +311,16 @@ public void recommendWaybleZoneTop20() throws Exception { ) .andExpect(status().is2xxSuccessful()) .andReturn(); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms (상위 20개)"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); diff --git a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java index c74d111c..1055fe88 100644 --- a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java @@ -80,7 +80,7 @@ public class WaybleZoneSearchApiIntegrationTest { private String token; - private static final int SAMPLES = 1000; + private static final int SAMPLES = 10000; List nameList = new ArrayList<>(Arrays.asList( "던킨도너츠", @@ -103,7 +103,7 @@ public class WaybleZoneSearchApiIntegrationTest { "내곡동" )); - @BeforeAll + @BeforeEach public void setup() { User testUser = User.createUserWithDetails( "testUser", "testUsername", UUID.randomUUID() + "@email", "password", @@ -142,8 +142,8 @@ public void setup() { .build(); User user = User.createUserWithDetails( - "user" + i, - "username" + i, + "u" + i, + "n" + i, UUID.randomUUID() + "@email", "password" + i, generateRandomBirthDate(), @@ -176,7 +176,7 @@ public void setup() { } } - @AfterAll + @AfterEach public void teardown() { waybleZoneVisitLogRepository.deleteAll(); waybleZoneDocumentRepository.deleteAll(); @@ -188,7 +188,6 @@ public void teardown() { public void checkDataExists() { List all = waybleZoneDocumentRepository.findAll(); - assertThat(all.size()).isGreaterThan(0); System.out.println("Total documents: " + all.size()); for (WaybleZoneDocument doc : all) { @@ -201,6 +200,9 @@ public void checkDataExists() { @Test @DisplayName("좌표를 전달받아 반경 이내의 웨이블 존을 거리 순으로 조회") public void findWaybleZoneByDistanceAscending() throws Exception{ + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl + "/maps") .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) @@ -210,12 +212,17 @@ public void findWaybleZoneByDistanceAscending() throws Exception{ ) .andExpect(status().is2xxSuccessful()) .andReturn(); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode node = root.get("data"); JsonNode dataNode = node.get("content"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -259,6 +266,10 @@ public void findWaybleZoneByDistanceAscending() throws Exception{ @DisplayName("특정 단어가 포함된 웨이블존을 거리 순으로 반환") public void findWaybleZoneByNameAscending() throws Exception{ final String word = nameList.get((int) (Math.random() * nameList.size())).substring(0, 2); + + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl + "/maps") .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) @@ -269,14 +280,17 @@ public void findWaybleZoneByNameAscending() throws Exception{ ) .andExpect(status().is2xxSuccessful()) .andReturn(); - - System.out.println(result.getResponse().getContentAsString(StandardCharsets.UTF_8)); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode node = root.get("data"); JsonNode dataNode = node.get("content"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -313,6 +327,7 @@ public void findWaybleZoneByNameAscending() throws Exception{ @DisplayName("특정 타입의 웨이블존을 거리 순으로 반환") public void findWaybleZoneByZoneTypeAscending() throws Exception{ final WaybleZoneType zoneType = WaybleZoneType.CAFE; + long startTime = System.currentTimeMillis(); MvcResult result = mockMvc.perform(get(baseUrl + "/maps") .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) @@ -324,11 +339,15 @@ public void findWaybleZoneByZoneTypeAscending() throws Exception{ .andExpect(status().is2xxSuccessful()) .andReturn(); + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; + String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode node = root.get("data"); JsonNode dataNode = node.get("content"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -366,6 +385,7 @@ public void findWaybleZoneByZoneTypeAscending() throws Exception{ public void findWaybleZoneByNameAndZoneTypeAscending() throws Exception{ final String word = nameList.get((int) (Math.random() * nameList.size())).substring(0, 2); final WaybleZoneType zoneType = WaybleZoneType.CAFE; + long startTime = System.currentTimeMillis(); MvcResult result = mockMvc.perform(get(baseUrl + "/maps") .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) @@ -378,11 +398,14 @@ public void findWaybleZoneByNameAndZoneTypeAscending() throws Exception{ .andExpect(status().is2xxSuccessful()) .andReturn(); + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode node = root.get("data"); JsonNode dataNode = node.get("content"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -420,6 +443,7 @@ public void findWaybleZoneByNameAndZoneTypeAscending() throws Exception{ @DisplayName("특정 동 주변 Top3 웨이블존 검색순 기반 검색") public void findMostSearchesWaybleZoneByDistrict() throws Exception{ final String district = districtList.get((int) (Math.random() * districtList.size())); + long startTime = System.currentTimeMillis(); MvcResult result = mockMvc.perform(get(baseUrl + "/district/most-searches") .header("Authorization", "Bearer " + token) .param("district", district) @@ -427,11 +451,14 @@ public void findMostSearchesWaybleZoneByDistrict() throws Exception{ ) .andExpect(status().is2xxSuccessful()) .andReturn(); + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -448,7 +475,6 @@ public void findMostSearchesWaybleZoneByDistrict() throws Exception{ // 검증: 각 결과의 필수 필드들이 존재하는지 확인 for (WaybleZoneDistrictResponseDto dto : dtoList) { assertThat(dto.visitCount()).isNotNull(); - assertThat(dto.visitCount()).isGreaterThan(0L); // 필수 필드들이 존재하는지 확인 assertThat(dto.waybleZoneInfo().zoneId()).isNotNull(); @@ -477,6 +503,7 @@ public void findMostSearchesWaybleZoneByDistrict() throws Exception{ @DisplayName("특정 동 주변 Top3 웨이블존 즐겨찾기순 기반 검색") public void findMostLikesWaybleZoneByDistrict() throws Exception{ final String district = districtList.get((int) (Math.random() * districtList.size())); + long startTime = System.currentTimeMillis(); MvcResult result = mockMvc.perform(get(baseUrl + "/district/most-likes") .header("Authorization", "Bearer " + token) .param("district", district) @@ -484,11 +511,14 @@ public void findMostLikesWaybleZoneByDistrict() throws Exception{ ) .andExpect(status().is2xxSuccessful()) .andReturn(); + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -558,6 +588,7 @@ public void findIsValidWaybleZoneTest () throws Exception{ JsonNode root = objectMapper.readTree(json); JsonNode dataNode = root.get("data"); + System.out.println("==== 성능 측정 결과 ====\n 응답 시간: " + responseTime + "ms"); System.out.println("==== 응답 결과 ===="); System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); @@ -589,13 +620,6 @@ public void findIsValidWaybleZoneTest () throws Exception{ name -> assertThat(name.replaceAll("\\s+", "")).contains(requestedName.replaceAll("\\s+", "")), name -> assertThat(requestedName).contains(name.substring(0, Math.min(2, name.length()))) ); - - System.out.println("==== 성능 측정 결과 ===="); - System.out.println(" 응답 시간: " + responseTime + "ms"); - System.out.println(" 요청한 이름: " + requestedName); - System.out.println(" 찾은 이름: " + foundName); - System.out.println(" 거리: " + String.format("%.3f km", dto.distance())); - System.out.println(" 위치: " + infoDto.latitude() + ", " + infoDto.longitude()); } private double haversine(double lat1, double lon1, double lat2, double lon2) { From 1d130ef17332e3582f2e243258df5276975ba8c8 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 26 Aug 2025 16:39:14 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[test]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/explore/service/WaybleZoneRecommendService.java | 3 +++ .../server/explore/WaybleFacilityApiIntegrationTest.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/explore/service/WaybleZoneRecommendService.java b/src/main/java/com/wayble/server/explore/service/WaybleZoneRecommendService.java index f893b44f..e4979a18 100644 --- a/src/main/java/com/wayble/server/explore/service/WaybleZoneRecommendService.java +++ b/src/main/java/com/wayble/server/explore/service/WaybleZoneRecommendService.java @@ -4,6 +4,7 @@ import com.wayble.server.explore.entity.WaybleZoneDocument; import com.wayble.server.explore.repository.RecommendLogDocumentRepository; import com.wayble.server.explore.repository.WaybleZoneDocumentRepository; +import com.wayble.server.explore.repository.recommend.WaybleZoneQueryRecommendMysqlRepository; import com.wayble.server.explore.repository.recommend.WaybleZoneQueryRecommendRepository; import com.wayble.server.common.exception.ApplicationException; import com.wayble.server.explore.dto.recommend.WaybleZoneRecommendResponseDto; @@ -26,6 +27,8 @@ public class WaybleZoneRecommendService { private final WaybleZoneQueryRecommendRepository waybleZoneRecommendRepository; + //private final WaybleZoneQueryRecommendMysqlRepository waybleZoneRecommendRepository; + private final RecommendLogDocumentRepository recommendLogDocumentRepository; private final WaybleZoneDocumentRepository waybleZoneDocumentRepository; diff --git a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java index 4bbe5cb6..98b5cd42 100644 --- a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java @@ -64,7 +64,7 @@ public class WaybleFacilityApiIntegrationTest { private static final double RADIUS = 20.0; - private static final int SAMPLES = 10000; + private static final int SAMPLES = 1000; private static final String baseUrl = "/api/v1/facilities/search";