diff --git a/.github/workflows/cd-develop.yml b/.github/workflows/cd-develop.yml index f13a44fd..a297d3ff 100644 --- a/.github/workflows/cd-develop.yml +++ b/.github/workflows/cd-develop.yml @@ -49,18 +49,56 @@ jobs: - name: Pull latest image from Docker Hub run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest - - name: Stop existing container if any + - name: Stop existing containers if any run: | - CONTAINER_ID=$(sudo docker ps -q --filter "name=github-actions-demo") - if [ ! -z "$CONTAINER_ID" ]; then - sudo docker stop $CONTAINER_ID + # Stop Spring Boot app container + APP_CONTAINER_ID=$(sudo docker ps -q --filter "name=github-actions-demo") + if [ ! -z "$APP_CONTAINER_ID" ]; then + sudo docker stop $APP_CONTAINER_ID fi + + # Stop Elasticsearch container + ES_CONTAINER_ID=$(sudo docker ps -q --filter "name=elasticsearch") + if [ ! -z "$ES_CONTAINER_ID" ]; then + sudo docker stop $ES_CONTAINER_ID + fi + + - name: Create Docker network if not exists + run: | + sudo docker network create wayble-network || true - - name: Run new container + - name: Run Elasticsearch container + run: | + sudo docker run -d \ + --name elasticsearch \ + --network wayble-network \ + -p 9200:9200 -p 9300:9300 \ + -e "discovery.type=single-node" \ + -e "xpack.security.enabled=false" \ + -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ + --restart unless-stopped \ + docker.elastic.co/elasticsearch/elasticsearch:9.0.2 + + - name: Wait for Elasticsearch to be ready + run: | + echo "Waiting for Elasticsearch to start..." + for i in {1..30}; do + if curl -s http://localhost:9200/_health > /dev/null 2>&1; then + echo "Elasticsearch is ready!" + break + fi + echo "Waiting... ($i/30)" + sleep 10 + done + + - name: Run new Spring Boot container run: | sudo docker run -d --rm \ --name github-actions-demo \ + --network wayble-network \ -p 8080:8080 \ + -e "ELASTICSEARCH_HOST=elasticsearch" \ + -e "ELASTICSEARCH_PORT=9200" \ ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo:latest - name: Prune unused docker resources diff --git a/.github/workflows/test-elasticsearch.yml b/.github/workflows/test-elasticsearch.yml new file mode 100644 index 00000000..c58524d8 --- /dev/null +++ b/.github/workflows/test-elasticsearch.yml @@ -0,0 +1,153 @@ +name: Test Elasticsearch CI/CD + +on: + workflow_dispatch: # 수동 실행 전용 + push: + branches: [ "feature/seungmin" ] # 본인 브랜치에서만 실행 + +permissions: + contents: read + +jobs: + # 1. 도커 이미지 빌드 및 푸시 + build-docker-image: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build with Gradle + uses: gradle/gradle-build-action@v2 + with: + arguments: clean bootJar + + - name: Docker build with test tag + run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/wayble-test:latest . + + - name: Docker login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} + + - name: Push Docker image + run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/wayble-test:latest + + # 2. EC2에서 테스트 컨테이너 실행 + test-elasticsearch-on-ec2: + needs: build-docker-image + runs-on: self-hosted + + steps: + - name: Pull latest test image from Docker Hub + run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/wayble-test:latest + + - name: Stop existing test containers if any + run: | + # Stop test app container + TEST_APP_CONTAINER_ID=$(sudo docker ps -q --filter "name=wayble-test-app") + if [ ! -z "$TEST_APP_CONTAINER_ID" ]; then + sudo docker stop $TEST_APP_CONTAINER_ID + sudo docker rm $TEST_APP_CONTAINER_ID + fi + + # Stop test Elasticsearch container + TEST_ES_CONTAINER_ID=$(sudo docker ps -q --filter "name=elasticsearch-test") + if [ ! -z "$TEST_ES_CONTAINER_ID" ]; then + sudo docker stop $TEST_ES_CONTAINER_ID + sudo docker rm $TEST_ES_CONTAINER_ID + fi + + - name: Create test Docker network if not exists + run: | + sudo docker network create wayble-test-network || true + + - name: Run test Elasticsearch container + run: | + sudo docker run -d \ + --name elasticsearch-test \ + --network wayble-test-network \ + -p 9201:9200 -p 9301:9300 \ + -e "discovery.type=single-node" \ + -e "xpack.security.enabled=false" \ + -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ + docker.elastic.co/elasticsearch/elasticsearch:9.0.2 + + - name: Wait for test Elasticsearch to be ready + run: | + echo "Waiting for test Elasticsearch to start..." + for i in {1..30}; do + if curl -s http://localhost:9201/_cluster/health > /dev/null 2>&1; then + echo "Test Elasticsearch is ready!" + break + fi + echo "Waiting... ($i/30)" + sleep 10 + done + + - name: Run test Spring Boot container + run: | + sudo docker run -d \ + --name wayble-test-app \ + --network wayble-test-network \ + -p 8081:8080 \ + -e "ELASTICSEARCH_HOST=elasticsearch-test" \ + -e "ELASTICSEARCH_PORT=9200" \ + -e "SPRING_PROFILES_ACTIVE=develop" \ + ${{ secrets.DOCKERHUB_USERNAME }}/wayble-test:latest + + - name: Test application health + run: | + echo "Waiting for application to start..." + sleep 30 + + # 애플리케이션 헬스체크 + if curl -f http://localhost:8081/actuator/health > /dev/null 2>&1; then + echo "✅ Application is healthy!" + else + echo "❌ Application health check failed" + exit 1 + fi + + # Elasticsearch 연결 테스트 + if curl -f http://localhost:9201/_cluster/health > /dev/null 2>&1; then + echo "✅ Elasticsearch is accessible!" + else + echo "❌ Elasticsearch connection failed" + exit 1 + fi + + - name: Cleanup test containers + if: always() # 성공/실패 관계없이 항상 실행 + run: | + # 테스트 컨테이너들 정리 + sudo docker stop wayble-test-app elasticsearch-test || true + sudo docker rm wayble-test-app elasticsearch-test || true + sudo docker network rm wayble-test-network || true + + - name: Prune unused docker resources + run: sudo docker system prune -f + + # ✅ 테스트 성공 알림 + - name: Send test success notification + if: success() + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\": \"✅ Elasticsearch CI/CD 테스트 성공! 이제 develop에 머지해도 안전합니다.\"}" \ + ${{ secrets.DISCORD_WEBHOOK_URL }} + + # ❌ 테스트 실패 알림 + - name: Send test failure notification + if: failure() + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\": \"❌ Elasticsearch CI/CD 테스트 실패! develop 머지 전에 수정이 필요합니다.\"}" \ + ${{ secrets.DISCORD_WEBHOOK_URL }} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3e0ad970..0db3be14 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0' + implementation 'com.amazonaws:aws-java-sdk-core:1.12.698' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.698' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..5b224110 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,45 @@ +version: '3.8' + +services: + # Spring Boot 애플리케이션 (테스트용) + app-test: + build: + context: . + dockerfile: Dockerfile + container_name: wayble-app-test + ports: + - "8081:8080" # 다른 포트 사용 + environment: + - SPRING_PROFILES_ACTIVE=develop + - ELASTICSEARCH_HOST=elasticsearch-test + - ELASTICSEARCH_PORT=9200 + depends_on: + - elasticsearch-test + networks: + - test-network + + # Elasticsearch (테스트용) + elasticsearch-test: + build: + context: . + dockerfile: Dockerfile.elasticsearch + container_name: elasticsearch-test + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - ES_JAVA_OPTS=-Xms512m -Xmx512m + ports: + - "9201:9200" # 다른 포트 사용 + - "9301:9300" + volumes: + - es-test-data:/usr/share/elasticsearch/data + networks: + - test-network + +networks: + test-network: + driver: bridge + +volumes: + es-test-data: + driver: local \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..a39eca89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + # Spring Boot 애플리케이션 + app: + build: + context: . + dockerfile: Dockerfile + container_name: wayble-app + restart: unless-stopped + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=develop + - ELASTICSEARCH_HOST=elasticsearch + - ELASTICSEARCH_PORT=9200 + depends_on: + - elasticsearch + networks: + - app-network + + # Elasticsearch + elasticsearch: + build: + context: . + dockerfile: Dockerfile.elasticsearch + container_name: elasticsearch + image: es-with-nori:9.0.2 + restart: unless-stopped + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - ES_JAVA_OPTS=-Xms512m -Xmx512m + ports: + - "9200:9200" + - "9300:9300" + volumes: + - esdata:/usr/share/elasticsearch/data + networks: + - app-network + +networks: + app-network: + driver: bridge + +volumes: + esdata: + driver: local \ No newline at end of file diff --git a/src/main/java/com/wayble/server/common/entity/BaseEntity.java b/src/main/java/com/wayble/server/common/entity/BaseEntity.java index be3badb6..9044360d 100644 --- a/src/main/java/com/wayble/server/common/entity/BaseEntity.java +++ b/src/main/java/com/wayble/server/common/entity/BaseEntity.java @@ -23,4 +23,8 @@ public class BaseEntity { @Column(name = "deleted_at") private LocalDateTime deletedAt; + + public void restore() { + this.deletedAt = null; + } } diff --git a/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java b/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java index 00141839..0ac6d6c6 100644 --- a/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java +++ b/src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java @@ -21,7 +21,7 @@ public class WaybleZoneSearchController { private final WaybleZoneSearchService waybleZoneSearchService; - @GetMapping("") + @GetMapping("/maps") public CommonResponse> findByCondition( @Valid @ModelAttribute WaybleZoneSearchConditionDto conditionDto, @RequestParam(name = "page", defaultValue = "0") int page, diff --git a/src/main/java/com/wayble/server/explore/dto/FacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/FacilityResponseDto.java new file mode 100644 index 00000000..b80eb4b9 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/FacilityResponseDto.java @@ -0,0 +1,29 @@ +package com.wayble.server.explore.dto; + +import com.wayble.server.explore.entity.EsWaybleZoneFacility; +import lombok.Builder; + +@Builder +public record FacilityResponseDto( + Boolean hasSlope, + Boolean hasNoDoorStep, + Boolean hasElevator, + Boolean hasTableSeat, + Boolean hasDisabledToilet, + String floorInfo +) { + public static FacilityResponseDto from(EsWaybleZoneFacility facility) { + if (facility == null) { + return null; + } + + return FacilityResponseDto.builder() + .hasSlope(facility.isHasSlope()) + .hasNoDoorStep(facility.isHasNoDoorStep()) + .hasElevator(facility.isHasElevator()) + .hasTableSeat(facility.isHasTableSeat()) + .hasDisabledToilet(facility.isHasDisabledToilet()) + .floorInfo(facility.getFloorInfo()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendResponseDto.java b/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendResponseDto.java index 051c0495..743519b1 100644 --- a/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendResponseDto.java @@ -1,5 +1,6 @@ package com.wayble.server.explore.dto.recommend; +import com.wayble.server.explore.dto.FacilityResponseDto; import com.wayble.server.explore.entity.WaybleZoneDocument; import com.wayble.server.wayblezone.entity.WaybleZoneType; import lombok.Builder; @@ -23,6 +24,8 @@ public record WaybleZoneRecommendResponseDto( Long reviewCount, + FacilityResponseDto facility, + Double distanceScore, Double similarityScore, @@ -42,6 +45,7 @@ public static WaybleZoneRecommendResponseDto from(WaybleZoneDocument waybleZoneD .reviewCount(waybleZoneDocument.getReviewCount()) .latitude(waybleZoneDocument.getAddress().getLocation().getLat()) .longitude(waybleZoneDocument.getAddress().getLocation().getLon()) + .facility(FacilityResponseDto.from(waybleZoneDocument.getFacility())) .distanceScore(0.0) .similarityScore(0.0) .recencyScore(0.0) diff --git a/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneDistrictResponseDto.java b/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneDistrictResponseDto.java new file mode 100644 index 00000000..9bf31c8f --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneDistrictResponseDto.java @@ -0,0 +1,35 @@ +package com.wayble.server.explore.dto.search; + +import com.wayble.server.explore.dto.FacilityResponseDto; +import com.wayble.server.explore.entity.WaybleZoneDocument; +import com.wayble.server.wayblezone.entity.WaybleZoneType; +import lombok.Builder; + +@Builder +public record WaybleZoneDistrictResponseDto( + Long zoneId, + String zoneName, + WaybleZoneType zoneType, + String thumbnailImageUrl, + Double latitude, + Double longitude, + Double averageRating, + Long reviewCount, + FacilityResponseDto facility, + Long visitCount +) { + public static WaybleZoneDistrictResponseDto from(WaybleZoneDocument document, Long visitCount) { + return WaybleZoneDistrictResponseDto.builder() + .zoneId(document.getZoneId()) + .zoneName(document.getZoneName()) + .zoneType(document.getZoneType()) + .thumbnailImageUrl(document.getThumbnailImageUrl()) + .latitude(document.getAddress().getLocation().getLat()) + .longitude(document.getAddress().getLocation().getLon()) + .averageRating(document.getAverageRating()) + .reviewCount(document.getReviewCount()) + .facility(FacilityResponseDto.from(document.getFacility())) + .visitCount(visitCount != null ? visitCount : 0L) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneDocumentRegisterDto.java b/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneDocumentRegisterDto.java index 67120fce..9017ca7f 100644 --- a/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneDocumentRegisterDto.java +++ b/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneDocumentRegisterDto.java @@ -1,6 +1,7 @@ package com.wayble.server.explore.dto.search; import com.wayble.server.common.entity.Address; +import com.wayble.server.wayblezone.entity.WaybleZoneFacility; import com.wayble.server.wayblezone.entity.WaybleZoneType; import lombok.Builder; @@ -11,6 +12,7 @@ public record WaybleZoneDocumentRegisterDto( WaybleZoneType waybleZoneType, String thumbnailImageUrl, Address address, + WaybleZoneFacility facility, Double averageRating, Long reviewCount ) { diff --git a/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneSearchResponseDto.java b/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneSearchResponseDto.java index aebf57ea..a6b10d48 100644 --- a/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneSearchResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/search/WaybleZoneSearchResponseDto.java @@ -1,5 +1,6 @@ package com.wayble.server.explore.dto.search; +import com.wayble.server.explore.dto.FacilityResponseDto; import com.wayble.server.explore.entity.WaybleZoneDocument; import com.wayble.server.wayblezone.entity.WaybleZoneType; import lombok.AccessLevel; @@ -24,7 +25,9 @@ public record WaybleZoneSearchResponseDto( Double averageRating, - Long reviewCount + Long reviewCount, + + FacilityResponseDto facility ) { public static WaybleZoneSearchResponseDto from(WaybleZoneDocument waybleZoneDocument, Double distance) { return WaybleZoneSearchResponseDto.builder() @@ -37,6 +40,7 @@ public static WaybleZoneSearchResponseDto from(WaybleZoneDocument waybleZoneDocu .distance(distance) .latitude(waybleZoneDocument.getAddress().getLocation().getLat()) .longitude(waybleZoneDocument.getAddress().getLocation().getLon()) + .facility(FacilityResponseDto.from(waybleZoneDocument.getFacility())) .build(); } } diff --git a/src/main/java/com/wayble/server/explore/entity/EsWaybleZoneFacility.java b/src/main/java/com/wayble/server/explore/entity/EsWaybleZoneFacility.java new file mode 100644 index 00000000..d1268c84 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/entity/EsWaybleZoneFacility.java @@ -0,0 +1,34 @@ +package com.wayble.server.explore.entity; + +import com.wayble.server.wayblezone.entity.WaybleZoneFacility; +import lombok.*; + +@ToString +@Builder(access = AccessLevel.PRIVATE) +@Getter +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EsWaybleZoneFacility { + + private boolean hasSlope; + private boolean hasNoDoorStep; + private boolean hasElevator; + private boolean hasTableSeat; + private boolean hasDisabledToilet; + private String floorInfo; + + public static EsWaybleZoneFacility from(WaybleZoneFacility facility) { + if (facility == null) { + return null; + } + + return EsWaybleZoneFacility.builder() + .hasSlope(facility.isHasSlope()) + .hasNoDoorStep(facility.isHasNoDoorStep()) + .hasElevator(facility.isHasElevator()) + .hasTableSeat(facility.isHasTableSeat()) + .hasDisabledToilet(facility.isHasDisabledToilet()) + .floorInfo(facility.getFloorInfo()) + .build(); + } +} diff --git a/src/main/java/com/wayble/server/explore/entity/WaybleZoneDocument.java b/src/main/java/com/wayble/server/explore/entity/WaybleZoneDocument.java index 09ad5e90..59fdbe1f 100644 --- a/src/main/java/com/wayble/server/explore/entity/WaybleZoneDocument.java +++ b/src/main/java/com/wayble/server/explore/entity/WaybleZoneDocument.java @@ -33,6 +33,9 @@ public class WaybleZoneDocument { @Field(type = FieldType.Object) private EsAddress address; + @Field(type = FieldType.Object) + private EsWaybleZoneFacility facility; + private double averageRating; private long reviewCount; @@ -42,8 +45,9 @@ public static WaybleZoneDocument fromEntity(WaybleZone waybleZone) { .zoneId(waybleZone.getId()) .zoneName(waybleZone.getZoneName()) .zoneType(waybleZone.getZoneType()) - .thumbnailImageUrl("thumbnail image url") // TODO: 이미지 경로 추가 + .thumbnailImageUrl(waybleZone.getMainImageUrl() != null ? waybleZone.getMainImageUrl() : null) // TODO: 이미지 경로 추가 .address(EsAddress.from(waybleZone.getAddress())) + .facility(EsWaybleZoneFacility.from(waybleZone.getFacility())) .averageRating(0.0) .reviewCount(0L) .build(); @@ -56,6 +60,7 @@ public static WaybleZoneDocument fromDto(WaybleZoneDocumentRegisterDto dto) { .zoneType(dto.waybleZoneType()) .thumbnailImageUrl(dto.thumbnailImageUrl()) .address(EsAddress.from(dto.address())) + .facility(EsWaybleZoneFacility.from(dto.facility())) .averageRating(dto.averageRating() != null ? dto.averageRating() : 0.0) .reviewCount(dto.reviewCount() != null ? dto.reviewCount() : 0L) .build(); diff --git a/src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java b/src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java index 8a2d65eb..0bd4f435 100644 --- a/src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java +++ b/src/main/java/com/wayble/server/explore/repository/recommend/WaybleZoneQueryRecommendRepository.java @@ -139,6 +139,7 @@ public List searchPersonalWaybleZones(User user, .longitude(zone.getAddress().getLocation().getLon()) .averageRating(zone.getAverageRating()) .reviewCount(zone.getReviewCount()) + .facility(com.wayble.server.explore.dto.FacilityResponseDto.from(zone.getFacility())) .distanceScore(distanceScore) .similarityScore(similarityScore) .recencyScore(recencyScore) diff --git a/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java index 05c66f38..2edbc03f 100644 --- a/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java +++ b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java @@ -4,8 +4,11 @@ import co.elastic.clients.elasticsearch._types.SortOptions; import co.elastic.clients.elasticsearch._types.SortOrder; import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.TermQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; import com.wayble.server.explore.dto.search.WaybleZoneSearchConditionDto; import com.wayble.server.explore.dto.search.WaybleZoneSearchResponseDto; +import com.wayble.server.explore.dto.search.WaybleZoneDistrictResponseDto; import com.wayble.server.explore.entity.WaybleZoneDocument; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -19,6 +22,10 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; +import java.util.stream.Collectors; @Repository @RequiredArgsConstructor @@ -121,4 +128,113 @@ public Slice searchWaybleZonesByCondition(WaybleZon return new SliceImpl<>(dtos, pageable, hasNext); } + + public List findTop3WaybleZonesByDistrict(String district) { + // 1. 특정 district에 속한 wayble zone들 조회 + List zoneIdsInDistrict = getZoneIdsByDistrict(district); + + if (zoneIdsInDistrict.isEmpty()) { + return Collections.emptyList(); + } + + // 2. 해당 zone들의 방문 로그 수 집계 + Map visitCountMap = getVisitCountsByZoneIds(zoneIdsInDistrict); + + // 3. 방문 수 기준으로 top3 선택 + List top3ZoneIds = visitCountMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(3) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // 4. top3 zone들의 상세 정보 조회 및 ResponseDto 생성 + return getWaybleZoneDetails(top3ZoneIds, visitCountMap); + } + + private List getZoneIdsByDistrict(String district) { + Query termQuery = TermQuery.of(t -> t + .field("address.district") + .value(district) + )._toQuery(); + + NativeQuery searchQuery = NativeQuery.builder() + .withQuery(termQuery) + .withMaxResults(1000) // district 내 모든 zone 조회 + .build(); + + SearchHits searchHits = operations.search( + searchQuery, + WaybleZoneDocument.class, + INDEX + ); + + return searchHits.getSearchHits().stream() + .map(hit -> hit.getContent().getZoneId()) + .collect(Collectors.toList()); + } + + private Map getVisitCountsByZoneIds(List zoneIds) { + Map visitCountMap = new HashMap<>(); + + // 각 zoneId별로 방문 로그 수를 직접 카운트 + for (Long zoneId : zoneIds) { + long count = countVisitLogsByZoneId(zoneId); + visitCountMap.put(zoneId, count); + } + + return visitCountMap; + } + + private long countVisitLogsByZoneId(Long zoneId) { + Query termQuery = TermQuery.of(t -> t + .field("zoneId") + .value(zoneId) + )._toQuery(); + + NativeQuery searchQuery = NativeQuery.builder() + .withQuery(termQuery) + .withMaxResults(0) // 카운트만 필요하므로 결과는 0개 + .build(); + + SearchHits searchHits = operations.search( + searchQuery, + Object.class, + IndexCoordinates.of("wayble_zone_visit_log") + ); + + return searchHits.getTotalHits(); + } + + private List getWaybleZoneDetails(List zoneIds, Map visitCountMap) { + if (zoneIds.isEmpty()) { + return Collections.emptyList(); + } + + BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder(); + for (Long zoneId : zoneIds) { + boolQueryBuilder.should(TermQuery.of(t -> t + .field("zoneId") + .value(zoneId) + )._toQuery()); + } + + NativeQuery searchQuery = NativeQuery.builder() + .withQuery(boolQueryBuilder.build()._toQuery()) + .withMaxResults(zoneIds.size()) + .build(); + + SearchHits searchHits = operations.search( + searchQuery, + WaybleZoneDocument.class, + INDEX + ); + + return searchHits.getSearchHits().stream() + .map(hit -> { + WaybleZoneDocument doc = hit.getContent(); + return WaybleZoneDistrictResponseDto.from(doc, visitCountMap.get(doc.getZoneId())); + }) + .sorted((a, b) -> Long.compare(b.visitCount(), a.visitCount())) // 방문 수 내림차순 정렬 + .collect(Collectors.toList()); + } } \ No newline at end of file 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 de826816..a9195d46 100644 --- a/src/main/java/com/wayble/server/explore/service/WaybleZoneRecommendService.java +++ b/src/main/java/com/wayble/server/explore/service/WaybleZoneRecommendService.java @@ -57,7 +57,7 @@ public List getWaybleZonePersonalRecommend(Long return recommendResponseDtoList; } - public Optional getTodayRecommendZone(Long userId) { + private Optional getTodayRecommendZone(Long userId) { LocalDate today = LocalDate.now(); Optional recommendLogDocument = recommendLogDocumentRepository.findByUserIdAndRecommendationDate(userId, today); @@ -72,7 +72,7 @@ public Optional getTodayRecommendZone(Long userI } } - public void saveRecommendLog(Long userId, Long zoneId) { + private void saveRecommendLog(Long userId, Long zoneId) { String logId = UUID.randomUUID().toString(); LocalDate dateNow = LocalDate.now(); @@ -88,7 +88,7 @@ public void saveRecommendLog(Long userId, Long zoneId) { recommendLogDocumentRepository.save(recommendLogDocument); } - public void updateRecommendLog(Long userId, Long zoneId) { + private void updateRecommendLog(Long userId, Long zoneId) { RecommendLogDocument recommendLogDocument = recommendLogDocumentRepository.findByUserIdAndZoneId(userId, zoneId) .orElseThrow(() -> new ApplicationException(RecommendErrorCase.RECOMMEND_LOG_NOT_EXIST)); diff --git a/src/main/java/com/wayble/server/user/repository/UserRepository.java b/src/main/java/com/wayble/server/user/repository/UserRepository.java index b375ea87..01289862 100644 --- a/src/main/java/com/wayble/server/user/repository/UserRepository.java +++ b/src/main/java/com/wayble/server/user/repository/UserRepository.java @@ -3,6 +3,8 @@ import com.wayble.server.user.entity.LoginType; import com.wayble.server.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; diff --git a/src/main/java/com/wayble/server/wayblezone/controller/WaybleZoneController.java b/src/main/java/com/wayble/server/wayblezone/controller/WaybleZoneController.java index a64923a1..51063d3c 100644 --- a/src/main/java/com/wayble/server/wayblezone/controller/WaybleZoneController.java +++ b/src/main/java/com/wayble/server/wayblezone/controller/WaybleZoneController.java @@ -13,6 +13,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -57,7 +58,8 @@ public CommonResponse getWaybleZoneDetail( ) { // TODO: JWT에서 userId 추출해서 상세 조회 기록 남기기 - // waybleZoneVisitLogService.saveVisitLog(null, waybleZoneId); + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + waybleZoneVisitLogService.saveVisitLog(userId, waybleZoneId); return CommonResponse.success(waybleZoneService.getWaybleZoneDetail(waybleZoneId)); } } \ No newline at end of file diff --git a/src/main/resources/elasticsearch/settings/wayble_zone_mappings.json b/src/main/resources/elasticsearch/settings/wayble_zone_mappings.json index cf56946e..48a09902 100644 --- a/src/main/resources/elasticsearch/settings/wayble_zone_mappings.json +++ b/src/main/resources/elasticsearch/settings/wayble_zone_mappings.json @@ -7,6 +7,29 @@ }, "address.location": { "type": "geo_point" + }, + "facility": { + "type": "object", + "properties": { + "hasSlope": { + "type": "boolean" + }, + "hasNoDoorStep": { + "type": "boolean" + }, + "hasElevator": { + "type": "boolean" + }, + "hasTableSeat": { + "type": "boolean" + }, + "hasDisabledToilet": { + "type": "boolean" + }, + "floorInfo": { + "type": "keyword" + } + } } } } \ No newline at end of file diff --git a/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java index 61b09b63..83fe14f5 100644 --- a/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleZoneRecommendApiIntegrationTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.wayble.server.common.config.security.jwt.JwtTokenProvider; import com.wayble.server.common.entity.Address; import com.wayble.server.explore.dto.recommend.WaybleZoneRecommendResponseDto; import com.wayble.server.explore.dto.search.WaybleZoneDocumentRegisterDto; @@ -19,6 +20,7 @@ import com.wayble.server.user.entity.User; import com.wayble.server.user.entity.UserType; import com.wayble.server.user.repository.UserRepository; +import com.wayble.server.wayblezone.entity.WaybleZoneFacility; import com.wayble.server.wayblezone.entity.WaybleZoneType; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; @@ -46,6 +48,9 @@ public class WaybleZoneRecommendApiIntegrationTest { @Autowired private MockMvc mockMvc; + @Autowired + private JwtTokenProvider jwtTokenProvider; + @Autowired private UserRepository userRepository; @@ -70,11 +75,13 @@ public class WaybleZoneRecommendApiIntegrationTest { private static final double RADIUS = 50.0; - private static final Long SAMPLES = 1000L; + private static final Long SAMPLES = 100L; private static final String baseUrl = "/api/v1/wayble-zones/recommend"; - private final Long userId = 1L; + private Long userId; + + private String token; List nameList = new ArrayList<>(Arrays.asList( "던킨도너츠", @@ -91,6 +98,15 @@ public class WaybleZoneRecommendApiIntegrationTest { @BeforeAll public void setup() { + User testUser = User.createUser( + "testUser", "testUsername", UUID.randomUUID() + "@email", "password", + generateRandomBirthDate(), Gender.MALE, LoginType.KAKAO, UserType.DISABLED + ); + + userRepository.save(testUser); + userId = testUser.getId(); + token = jwtTokenProvider.generateToken(userId, "ROLE_USER"); + for (int i = 1; i <= SAMPLES / 2; i++) { Long zoneId = (long) (Math.random() * SAMPLES) + 1; if(!recommendLogDocumentRepository.existsByUserIdAndZoneId(userId, zoneId)) { @@ -119,6 +135,8 @@ public void setup() { .longitude(points.get("longitude")) .build(); + WaybleZoneFacility facility = createRandomFacility(i); + WaybleZoneDocumentRegisterDto dto = WaybleZoneDocumentRegisterDto .builder() .zoneId((long) i) @@ -126,6 +144,7 @@ public void setup() { .address(address) .thumbnailImageUrl("thumbnail url" + i) .waybleZoneType(WaybleZoneType.values()[i % WaybleZoneType.values().length]) + .facility(facility) .averageRating(Math.random() * 5) .reviewCount((long)(Math.random() * 500)) .build(); @@ -135,12 +154,12 @@ public void setup() { User user = User.createUser( "user" + i, - "user" + i, - "user email" + i, - "user password" + i, + "username" + i, + UUID.randomUUID() + "@email", + "password" + i, generateRandomBirthDate(), Gender.values()[i % 2], - LoginType.BASIC, + LoginType.values()[i % LoginType.values().length], UserType.DISABLED ); userRepository.save(user); @@ -173,30 +192,24 @@ public void teardown() { @DisplayName("데이터 저장 테스트") public void checkDataExists() { List waybleZoneDocumentList = waybleZoneDocumentRepository.findAll(); - System.out.println("=== 웨이블존 목록 ==="); - assertThat(waybleZoneDocumentList.size()).isGreaterThan(0); + System.out.println("Total documents: " + waybleZoneDocumentList.size()); for (WaybleZoneDocument doc : waybleZoneDocumentList) { assertThat(doc.getZoneId()).isNotNull(); assertThat(doc.getZoneName()).isNotNull(); assertThat(doc.getAddress().getLocation()).isNotNull(); - System.out.println("존 정보: " + doc.toString()); - System.out.println("주소: " + doc.getAddress().toString()); } List waybleZoneVisitLogList = waybleZoneVisitLogDocumentRepository.findAll(); - System.out.println("=== 웨이블존 방문 목록 ==="); - assertThat(waybleZoneVisitLogList.size()).isGreaterThan(0); - for (WaybleZoneVisitLogDocument doc : waybleZoneVisitLogList) { - System.out.println("방문 정보" + doc.toString()); - } + System.out.println("visit log size: " + waybleZoneVisitLogList.size()); } @Test @DisplayName("추천 기록 저장 테스트") public void saveRecommendLogTest() throws Exception { MvcResult result = mockMvc.perform(get(baseUrl) + .header("Authorization", "Bearer " + token) .param("userId", String.valueOf(userId)) .param("latitude", String.valueOf(LATITUDE)) .param("longitude", String.valueOf(LONGITUDE)) @@ -224,19 +237,13 @@ public void saveRecommendLogTest() throws Exception { assertThat(recommendLogDocument.get().getUserId()).isEqualTo(userId); assertThat(recommendLogDocument.get().getZoneId()).isEqualTo(zoneId); assertThat(recommendLogDocument.get().getRecommendationDate()).isEqualTo(LocalDate.now()); - System.out.println("===recommend log==="); - System.out.println("id = " + recommendLogDocument.get().getId()); - System.out.println("userId = " +recommendLogDocument.get().getUserId()); - System.out.println("zoneId = " +recommendLogDocument.get().getZoneId()); - System.out.println("recommendationDate = " +recommendLogDocument.get().getRecommendationDate()); - System.out.println("recommendCount " +recommendLogDocument.get().getRecommendCount()); } @Test @DisplayName("추천 기능 테스트") public void recommendWaybleZone() throws Exception { - MvcResult result = mockMvc.perform(get(baseUrl) + .header("Authorization", "Bearer " + token) .param("userId", String.valueOf(userId)) .param("latitude", String.valueOf(LATITUDE)) .param("longitude", String.valueOf(LONGITUDE)) @@ -261,6 +268,13 @@ public void recommendWaybleZone() throws Exception { assertThat(dto.zoneType()).isNotNull(); assertThat(dto.latitude()).isNotNull(); assertThat(dto.longitude()).isNotNull(); + assertThat(dto.facility()).isNotNull(); + assertThat(dto.facility().hasSlope()).isNotNull(); + assertThat(dto.facility().hasNoDoorStep()).isNotNull(); + assertThat(dto.facility().hasElevator()).isNotNull(); + assertThat(dto.facility().hasTableSeat()).isNotNull(); + assertThat(dto.facility().hasDisabledToilet()).isNotNull(); + assertThat(dto.facility().floorInfo()).isNotNull(); System.out.println("zoneId = " + dto.zoneId()); System.out.println("zoneName = " + dto.zoneName()); @@ -275,16 +289,28 @@ public void recommendWaybleZone() throws Exception { System.out.println("similarityScore = " + dto.similarityScore()); System.out.println("recencyScore = " + dto.recencyScore()); System.out.println("totalScore = " + dto.totalScore()); + System.out.println("=== Facility Info ==="); + if (dto.facility() != null) { + System.out.println("hasSlope = " + dto.facility().hasSlope()); + System.out.println("hasNoDoorStep = " + dto.facility().hasNoDoorStep()); + System.out.println("hasElevator = " + dto.facility().hasElevator()); + System.out.println("hasTableSeat = " + dto.facility().hasTableSeat()); + System.out.println("hasDisabledToilet = " + dto.facility().hasDisabledToilet()); + System.out.println("floorInfo = " + dto.facility().floorInfo()); + } else { + System.out.println("facility info is null"); + } } @Test @DisplayName("추천 결과 상위 N개 값 테스트") public void recommendWaybleZoneTop20() throws Exception { MvcResult result = mockMvc.perform(get(baseUrl) + .header("Authorization", "Bearer " + token) .param("userId", String.valueOf(userId)) .param("latitude", String.valueOf(LATITUDE)) .param("longitude", String.valueOf(LONGITUDE)) - .param("count", String.valueOf(20)) + .param("size", String.valueOf(20)) .accept(MediaType.APPLICATION_JSON) ) .andExpect(status().is2xxSuccessful()) @@ -373,4 +399,19 @@ private LocalDate generateRandomBirthDate() { return start.plusDays(randomDays); } + + private WaybleZoneFacility createRandomFacility(int i) { + Random random = new Random(i); // 시드 고정으로 재현 가능한 랜덤 + + String[] floors = {"B1", "1층", "2층", "3층"}; + + return WaybleZoneFacility.builder() + .hasSlope(random.nextBoolean()) + .hasNoDoorStep(random.nextBoolean()) + .hasElevator(random.nextBoolean()) + .hasTableSeat(random.nextBoolean()) + .hasDisabledToilet(random.nextBoolean()) + .floorInfo(floors[random.nextInt(floors.length)]) + .build(); + } } diff --git a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java index ad299b58..5b38ba6e 100644 --- a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java @@ -3,11 +3,18 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.wayble.server.common.config.security.jwt.JwtTokenProvider; import com.wayble.server.common.entity.Address; import com.wayble.server.explore.dto.search.WaybleZoneDocumentRegisterDto; import com.wayble.server.explore.dto.search.WaybleZoneSearchResponseDto; import com.wayble.server.explore.entity.WaybleZoneDocument; import com.wayble.server.explore.repository.WaybleZoneDocumentRepository; +import com.wayble.server.user.entity.Gender; +import com.wayble.server.user.entity.LoginType; +import com.wayble.server.user.entity.User; +import com.wayble.server.user.entity.UserType; +import com.wayble.server.user.repository.UserRepository; +import com.wayble.server.wayblezone.entity.WaybleZoneFacility; import com.wayble.server.wayblezone.entity.WaybleZoneType; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; @@ -18,7 +25,10 @@ import org.springframework.test.web.servlet.MvcResult; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.offset; @@ -36,6 +46,12 @@ public class WaybleZoneSearchApiIntegrationTest { @Autowired private WaybleZoneDocumentRepository waybleZoneDocumentRepository; + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + @Autowired private ObjectMapper objectMapper; @@ -47,6 +63,10 @@ public class WaybleZoneSearchApiIntegrationTest { private static final String baseUrl = "/api/v1/wayble-zones/search"; + private Long userId; + + private String token; + List nameList = new ArrayList<>(Arrays.asList( "던킨도너츠", "베스킨라빈스", @@ -62,6 +82,15 @@ public class WaybleZoneSearchApiIntegrationTest { @BeforeAll public void setup() { + User testUser = User.createUser( + "testUser", "testUsername", UUID.randomUUID() + "@email", "password", + generateRandomBirthDate(), Gender.MALE, LoginType.KAKAO, UserType.DISABLED + ); + + userRepository.save(testUser); + userId = testUser.getId(); + token = jwtTokenProvider.generateToken(userId, "ROLE_USER"); + for (int i = 1; i <= 1000; i++) { Map points = makeRandomPoint(); Address address = Address.builder() @@ -74,12 +103,15 @@ public void setup() { .longitude(points.get("longitude")) .build(); + WaybleZoneFacility facility = createRandomFacility(i); + WaybleZoneDocumentRegisterDto dto = WaybleZoneDocumentRegisterDto .builder() .zoneId((long) i) .zoneName(nameList.get((int) (Math.random() * nameList.size()))) .address(address) .waybleZoneType(WaybleZoneType.values()[i % WaybleZoneType.values().length]) + .facility(facility) .averageRating(Math.random() * 5) .reviewCount((long)(Math.random() * 500)) .build(); @@ -92,28 +124,28 @@ public void setup() { @AfterAll public void teardown() { waybleZoneDocumentRepository.deleteAll(); + userRepository.deleteAll(); } @Test public void checkDataExists() { List all = waybleZoneDocumentRepository.findAll(); - System.out.println("=== 저장된 데이터 확인 ==="); - System.out.println("Total documents: " + all.size()); + assertThat(all.size()).isGreaterThan(0); + System.out.println("Total documents: " + all.size()); for (WaybleZoneDocument doc : all) { assertThat(doc.getZoneId()).isNotNull(); assertThat(doc.getZoneName()).isNotNull(); assertThat(doc.getAddress().getLocation()).isNotNull(); - System.out.println("존 정보: " + doc.toString()); - System.out.println("주소: " + doc.getAddress().toString()); } } @Test @DisplayName("좌표를 전달받아 반경 이내의 웨이블 존을 거리 순으로 조회") public void findWaybleZoneByDistanceAscending() throws Exception{ - MvcResult result = mockMvc.perform(get(baseUrl) + MvcResult result = mockMvc.perform(get(baseUrl + "/maps") + .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) .param("longitude", String.valueOf(LONGITUDE)) .param("radiusKm", String.valueOf(RADIUS)) @@ -150,10 +182,33 @@ public void findWaybleZoneByDistanceAscending() throws Exception{ dto.distance(), dtoList.get(i-1).distance()) .isGreaterThanOrEqualTo(dtoList.get(i - 1).distance()); } + + // facility 검증 추가 + assertThat(dto.facility()).isNotNull(); + assertThat(dto.facility().hasSlope()).isNotNull(); + assertThat(dto.facility().hasNoDoorStep()).isNotNull(); + assertThat(dto.facility().hasElevator()).isNotNull(); + assertThat(dto.facility().hasTableSeat()).isNotNull(); + assertThat(dto.facility().hasDisabledToilet()).isNotNull(); + assertThat(dto.facility().floorInfo()).isNotNull(); } - for (WaybleZoneSearchResponseDto dto : dtoList) { - System.out.println(dto.toString()); + + if (!dtoList.isEmpty()) { + WaybleZoneSearchResponseDto firstDto = dtoList.get(0); + System.out.println("=== Search Result - Facility Info ==="); + System.out.println("zoneId = " + firstDto.zoneId()); + System.out.println("zoneName = " + firstDto.zoneName()); + if (firstDto.facility() != null) { + System.out.println("hasSlope = " + firstDto.facility().hasSlope()); + System.out.println("hasNoDoorStep = " + firstDto.facility().hasNoDoorStep()); + System.out.println("hasElevator = " + firstDto.facility().hasElevator()); + System.out.println("hasTableSeat = " + firstDto.facility().hasTableSeat()); + System.out.println("hasDisabledToilet = " + firstDto.facility().hasDisabledToilet()); + System.out.println("floorInfo = " + firstDto.facility().floorInfo()); + } else { + System.out.println("facility info is null"); + } } } @@ -161,7 +216,8 @@ public void findWaybleZoneByDistanceAscending() throws Exception{ @DisplayName("특정 단어가 포함된 웨이블존을 거리 순으로 반환") public void findWaybleZoneByNameAscending() throws Exception{ final String word = nameList.get((int) (Math.random() * nameList.size())).substring(0, 2); - MvcResult result = mockMvc.perform(get(baseUrl) + MvcResult result = mockMvc.perform(get(baseUrl + "/maps") + .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) .param("longitude", String.valueOf(LONGITUDE)) .param("radiusKm", String.valueOf(RADIUS)) @@ -214,7 +270,8 @@ public void findWaybleZoneByNameAscending() throws Exception{ @DisplayName("특정 타입의 웨이블존을 거리 순으로 반환") public void findWaybleZoneByZoneTypeAscending() throws Exception{ final WaybleZoneType zoneType = WaybleZoneType.CAFE; - MvcResult result = mockMvc.perform(get(baseUrl) + MvcResult result = mockMvc.perform(get(baseUrl + "/maps") + .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) .param("longitude", String.valueOf(LONGITUDE)) .param("radiusKm", String.valueOf(RADIUS)) @@ -268,7 +325,8 @@ 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; - MvcResult result = mockMvc.perform(get(baseUrl) + MvcResult result = mockMvc.perform(get(baseUrl + "/maps") + .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(LATITUDE)) .param("longitude", String.valueOf(LONGITUDE)) .param("radiusKm", String.valueOf(RADIUS)) @@ -348,5 +406,31 @@ private Map makeRandomPoint() { return Map.of("latitude", randomLat, "longitude", randomLng); } + + private LocalDate generateRandomBirthDate() { + LocalDate today = LocalDate.now(); + LocalDate start = today.minusYears(90); // 90세 + LocalDate end = today.minusYears(10); // 10세 + + long daysBetween = ChronoUnit.DAYS.between(start, end); + long randomDays = ThreadLocalRandom.current().nextLong(daysBetween + 1); + + return start.plusDays(randomDays); + } + + private WaybleZoneFacility createRandomFacility(int i) { + Random random = new Random(i); // 시드 고정으로 재현 가능한 랜덤 + + String[] floors = {"B1", "1층", "2층", "3층"}; + + return WaybleZoneFacility.builder() + .hasSlope(random.nextBoolean()) + .hasNoDoorStep(random.nextBoolean()) + .hasElevator(random.nextBoolean()) + .hasTableSeat(random.nextBoolean()) + .hasDisabledToilet(random.nextBoolean()) + .floorInfo(floors[random.nextInt(floors.length)]) + .build(); + } }