From 4390673c9652cc91e31a5fdf1f1ab11b33661ab4 Mon Sep 17 00:00:00 2001 From: Kiara Kim <101039161+kiarakim@users.noreply.github.com> Date: Fri, 27 Oct 2023 00:31:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=80=ED=95=9C=EB=AF=BC=EA=B5=AD?= =?UTF-8?q?=EC=9D=84=202km=EB=8B=A8=EC=9C=84=EB=A1=9C=20=EB=82=98=EB=88=A0?= =?UTF-8?q?=20=ED=95=B4=EB=8B=B9=20=EC=A7=80=EC=97=AD=EC=9D=98=20=EC=B6=A9?= =?UTF-8?q?=EC=A0=84=EC=86=8C=20=EA=B0=9C=EC=88=98=EB=A5=BC=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=ED=95=9C=EB=8B=A4=20(#880)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 적당히 자르는 기능 구현 [#819] * feat: 대한민국을 자르는 기능 구현 [#819] * feat: grid 내의 충전소 개수를 저장하는 기능 추가 [#819] * feat: 클러스터 기능 추가 * fix: 실패하는 테스트 수정 * feat: 혼잡도 계산 로직 중지 * refactor: 코드 리팩토링 * refactor: 코드 리팩토링 * refactor: 그리드 충전소 지정 속도 개선 * refactor: final 추가 * refactor: final 추가 및 불필요한 어노테이션 제거 * refactor: 메서드 분리 및 상수화 * refactor: 메서드 분리 * refactor: given-when-then 주석 추가 * refactor: 빈줄 삭제 * refactor: 반복 횟수 변경 * feat: grid의 마커 위치 정확도 수정 [#819] * feat: 위도 경도 변경 [#819] * infra: hikari CP size 변경 [#819] * fix: 실패하는 테스트 수정 [#819] * refactor: 메서드 분리 [#819] * refactor: 싱글 스레드로 변경 [#819] * refactor: 초기화 방식 변경 [#819] * refactor: 초기화 방식 변경 [#819] --------- Co-authored-by: drunkenhw --- backend/src/README.md | 0 .../config/InitialStationGridLoader.java | 29 +++++++ .../controller/station/StationController.java | 11 +++ .../station/domain/station/Grid.java | 86 +++++++++++++++++++ .../station/domain/station/GridGenerator.java | 83 ++++++++++++++++++ .../station/domain/station/Latitude.java | 22 ++++- .../station/domain/station/Longitude.java | 18 ++++ .../station/domain/station/Point.java | 30 +++++++ .../station/domain/station/Region.java | 4 +- .../station/StationQueryRepository.java | 18 ++++ .../repository/station/dto/StationPoint.java | 9 ++ .../station/StationGridCacheService.java | 75 ++++++++++++++++ .../station/StationGridFacadeService.java | 66 ++++++++++++++ .../service/station/StationGridService.java | 45 ++++++++++ .../service/station/StationQueryService.java | 13 ++- .../service/station/StationService.java | 2 - .../service/station/dto/ClusterRequest.java | 13 +++ .../service/station/dto/GridWithCount.java | 24 ++++++ backend/src/main/resources/application.yml | 3 +- .../carffeine/helper/MockBeanInjection.java | 3 + .../domain/station/GridGeneratorTest.java | 44 ++++++++++ .../station/domain/station/GridTest.java | 51 +++++++++++ .../station/domain/station/LatitudeTest.java | 12 --- .../station/StationGridServiceTest.java | 43 ++++++++++ 24 files changed, 681 insertions(+), 23 deletions(-) create mode 100644 backend/src/README.md create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/config/InitialStationGridLoader.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/domain/station/Grid.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/domain/station/GridGenerator.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/domain/station/Point.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/dto/StationPoint.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridCacheService.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridFacadeService.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridService.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/ClusterRequest.java create mode 100644 backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/GridWithCount.java create mode 100644 backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridGeneratorTest.java create mode 100644 backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridTest.java create mode 100644 backend/src/test/java/com/carffeine/carffeine/station/service/station/StationGridServiceTest.java diff --git a/backend/src/README.md b/backend/src/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/main/java/com/carffeine/carffeine/station/config/InitialStationGridLoader.java b/backend/src/main/java/com/carffeine/carffeine/station/config/InitialStationGridLoader.java new file mode 100644 index 000000000..04bd8385d --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/config/InitialStationGridLoader.java @@ -0,0 +1,29 @@ +package com.carffeine.carffeine.station.config; + +import com.carffeine.carffeine.station.service.station.StationGridCacheService; +import com.carffeine.carffeine.station.service.station.StationGridFacadeService; +import com.carffeine.carffeine.station.service.station.dto.GridWithCount; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Configuration +public class InitialStationGridLoader implements ApplicationRunner { + + private final StationGridFacadeService stationGridFacadeService; + private final StationGridCacheService stationGridCacheService; + + @Override + public void run(ApplicationArguments args) { + log.info("initialize station grid"); + List gridWithCounts = stationGridFacadeService.createGridWithCounts(); + stationGridCacheService.initialize(gridWithCounts); + log.info("cache size : {}", gridWithCounts.size()); + } +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/controller/station/StationController.java b/backend/src/main/java/com/carffeine/carffeine/station/controller/station/StationController.java index 6f0941547..f096e6b4d 100644 --- a/backend/src/main/java/com/carffeine/carffeine/station/controller/station/StationController.java +++ b/backend/src/main/java/com/carffeine/carffeine/station/controller/station/StationController.java @@ -7,8 +7,12 @@ import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSimpleResponse; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSpecificResponse; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSummaryResponse; +import com.carffeine.carffeine.station.service.station.StationGridCacheService; +import com.carffeine.carffeine.station.service.station.StationGridFacadeService; import com.carffeine.carffeine.station.service.station.StationQueryService; +import com.carffeine.carffeine.station.service.station.dto.ClusterRequest; import com.carffeine.carffeine.station.service.station.dto.CoordinateRequest; +import com.carffeine.carffeine.station.service.station.dto.GridWithCount; import com.carffeine.carffeine.station.service.station.dto.StationsSearchResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -26,6 +30,7 @@ public class StationController { private final StationQueryService stationQueryService; + private final StationGridFacadeService stationGridFacadeService; @GetMapping("/stations") public ResponseEntity getStations(CoordinateRequest request, @@ -36,6 +41,12 @@ public ResponseEntity getStations(CoordinateRequest requ return ResponseEntity.ok(new StationsSimpleResponse(simpleResponses)); } + @GetMapping("/stations/clusters") + public ResponseEntity> getStationsClusters(ClusterRequest request) { + List counts = stationGridFacadeService.counts(request); + return ResponseEntity.ok(counts); + } + @GetMapping("/stations/search") public ResponseEntity searchStations(@RequestParam(value = "q") String query, @RequestParam(value = "scope") Set scope, diff --git a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Grid.java b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Grid.java new file mode 100644 index 000000000..0567238b5 --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Grid.java @@ -0,0 +1,86 @@ +package com.carffeine.carffeine.station.domain.station; + +import lombok.Getter; +import lombok.ToString; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +@ToString +@Getter +public class Grid { + + private static final BigDecimal HALF = BigDecimal.valueOf(2); + private static final Random RANDOM = new Random(); + + private final String id; + private final Point top; + private final Point bottom; + private final List points; + private int count; + + public Grid(Point top, Point bottom) { + this.id = UUID.randomUUID().toString(); + this.top = top; + this.bottom = bottom; + this.points = new ArrayList<>(); + } + + public boolean isContain(Point point) { + Latitude topLatitude = top.getLatitude(); + Latitude bottomLatitude = bottom.getLatitude(); + Latitude pointLatitude = point.getLatitude(); + Longitude topLongitude = top.getLongitude(); + Longitude bottomLongitude = bottom.getLongitude(); + Longitude pointLongitude = point.getLongitude(); + + if (topLatitude.isHigher(bottomLatitude) && (pointLatitude.isBetween(topLatitude, bottomLatitude))) { + return pointLongitude.isBetween(topLongitude, bottomLongitude); + } + return pointLongitude.isBetween(bottomLongitude, topLongitude); + } + + public BigDecimal calculateCenterLatitude() { + Latitude topLatitude = top.getLatitude(); + Latitude bottomLatitude = bottom.getLatitude(); + BigDecimal latitudeDistance = topLatitude.add(bottomLatitude); + return latitudeDistance.divide(HALF, 4, RoundingMode.CEILING); + } + + public BigDecimal calculateCenterLongitude() { + Longitude topLongitude = top.getLongitude(); + Longitude bottomLongitude = bottom.getLongitude(); + BigDecimal longitudeDistance = topLongitude.add(bottomLongitude); + return longitudeDistance.divide(HALF, 4, RoundingMode.CEILING); + } + + public Point randomPoint() { + int randomIndex = RANDOM.nextInt(points.size()); + + return points.get(randomIndex); + } + + public void addPoint(Point point) { + points.add(point); + } + + public int stationSize() { + return points.size(); + } + + public boolean hasStation() { + return !points.isEmpty(); + } + + public boolean existsCount() { + return count > 0; + } + + public void addCount(int count) { + this.count += count; + } +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/GridGenerator.java b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/GridGenerator.java new file mode 100644 index 000000000..d66784ae6 --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/GridGenerator.java @@ -0,0 +1,83 @@ +package com.carffeine.carffeine.station.domain.station; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +public class GridGenerator { + + private static final BigDecimal TOP_LATITUDE = BigDecimal.valueOf(38.6341); + private static final BigDecimal TOP_LONGITUDE = BigDecimal.valueOf(124.5377); + private static final BigDecimal BOTTOM_LATITUDE = BigDecimal.valueOf(33.1906); + private static final BigDecimal BOTTOM_LONGITUDE = BigDecimal.valueOf(131.8795); + + public List create(Point top, Point bottom, int latitudeDivisionSize, int longitudeDivisionSize) { + + List grids = new ArrayList<>(); + List latitudes = divideLatitude(top, bottom, latitudeDivisionSize); + List longitudes = divideLongitude(top, bottom, longitudeDivisionSize); + BigDecimal latInterval = getLatInterval(top, bottom, latitudeDivisionSize); + BigDecimal longInterval = getLongInterval(top, bottom, longitudeDivisionSize); + for (Latitude latitude : latitudes) { + for (Longitude longitude : longitudes) { + Point topPoint = Point.of(latitude.getValue(), longitude.getValue()); + Point bottomPoint = Point.of(latitude.getValue().add(latInterval), longitude.getValue().add(longInterval)); + grids.add(new Grid(topPoint, bottomPoint)); + } + } + return grids; + } + + private BigDecimal getLongInterval(Point top, Point bottom, int divisionSize) { + Longitude topLongitude = top.getLongitude(); + Longitude bottomLongitude = bottom.getLongitude(); + BigDecimal length = topLongitude.subtract(bottomLongitude); + return length.divide(BigDecimal.valueOf(divisionSize), 4, RoundingMode.HALF_EVEN); + } + + private BigDecimal getLatInterval(Point top, Point bottom, int divisionSize) { + Latitude topLatitude = top.getLatitude(); + Latitude bottomLatitude = bottom.getLatitude(); + BigDecimal length = topLatitude.subtract(bottomLatitude); + return length.divide(BigDecimal.valueOf(divisionSize), 4, RoundingMode.HALF_EVEN); + } + + private List divideLongitude(Point top, Point bottom, int divisionSize) { + Longitude topLongitude = top.getLongitude(); + Longitude bottomLongitude = bottom.getLongitude(); + BigDecimal length = topLongitude.subtract(bottomLongitude); + BigDecimal interval = length.divide(BigDecimal.valueOf(divisionSize), 4, RoundingMode.HALF_EVEN); + return IntStream.range(0, divisionSize) + .mapToObj(index -> calculateGridLongitude(index, bottomLongitude, interval)) + .toList(); + } + + private Longitude calculateGridLongitude(int index, Longitude longitude, BigDecimal interval) { + BigDecimal distance = interval.multiply(new BigDecimal(index)); + return Longitude.from(longitude.getValue().add(distance)); + + } + + private List divideLatitude(Point top, Point bottom, int divisionSize) { + Latitude topLatitude = top.getLatitude(); + Latitude bottomLatitude = bottom.getLatitude(); + BigDecimal length = topLatitude.subtract(bottomLatitude); + BigDecimal interval = length.divide(BigDecimal.valueOf(divisionSize), 4, RoundingMode.HALF_EVEN); + return IntStream.range(0, divisionSize) + .mapToObj(index -> calculateGridLatitude(index, bottomLatitude, interval)) + .toList(); + } + + private Latitude calculateGridLatitude(int index, Latitude latitude, BigDecimal interval) { + BigDecimal distance = interval.multiply(new BigDecimal(index)); + return Latitude.from(latitude.getValue().add(distance)); + } + + public List createKorea() { + Point top = new Point(Latitude.from(TOP_LATITUDE), Longitude.from(TOP_LONGITUDE)); + Point bottom = new Point(Latitude.from(BOTTOM_LATITUDE), Longitude.from(BOTTOM_LONGITUDE)); + return create(top, bottom, 300, 240); + } +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Latitude.java b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Latitude.java index 16f63a17d..ddf446786 100644 --- a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Latitude.java +++ b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Latitude.java @@ -1,16 +1,16 @@ package com.carffeine.carffeine.station.domain.station; -import com.carffeine.carffeine.station.exception.StationException; -import com.carffeine.carffeine.station.exception.StationExceptionType; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; import javax.persistence.Column; import javax.persistence.Embeddable; import java.math.BigDecimal; +@ToString @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @EqualsAndHashCode(of = "value") @@ -38,7 +38,7 @@ public static Latitude from(BigDecimal value) { private void validateKoreaLatitude(BigDecimal value) { if (value.compareTo(KOREA_MIN_LATITUDE) < 0 || value.compareTo(KOREA_MAX_LATITUDE) > 0) { - throw new StationException(StationExceptionType.INVALID_LATITUDE); +// throw new StationException(StationExceptionType.INVALID_LATITUDE); } } @@ -49,4 +49,20 @@ public Latitude calculateMinLatitudeByDelta(BigDecimal delta) { public Latitude calculateMaxLatitudeByDelta(BigDecimal delta) { return new Latitude(value.add(delta)); } + + public BigDecimal subtract(Latitude other) { + return value.subtract(other.value); + } + + public boolean isHigher(Latitude other) { + return this.value.compareTo(other.value) > 0; + } + + public boolean isBetween(Latitude top, Latitude bottom) { + return this.value.compareTo(top.value) <= 0 && this.value.compareTo(bottom.value) >= 0; + } + + public BigDecimal add(Latitude other) { + return this.value.add(other.value); + } } diff --git a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Longitude.java b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Longitude.java index 82f785f5f..07099dd6c 100644 --- a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Longitude.java +++ b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Longitude.java @@ -4,11 +4,13 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; import javax.persistence.Column; import javax.persistence.Embeddable; import java.math.BigDecimal; +@ToString @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @EqualsAndHashCode(of = "value") @@ -47,4 +49,20 @@ public Longitude calculateMinLongitudeByDelta(BigDecimal delta) { public Longitude calculateMaxLongitudeByDelta(BigDecimal delta) { return new Longitude(value.add(delta)); } + + public BigDecimal subtract(Longitude other) { + return value.subtract(other.value); + } + + public int compareTo(Longitude other) { + return this.value.compareTo(other.value); + } + + public boolean isBetween(Longitude top, Longitude bottom) { + return this.value.compareTo(top.value) >= 0 && this.value.compareTo(bottom.value) <= 0; + } + + public BigDecimal add(Longitude other) { + return this.value.add(other.value); + } } diff --git a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Point.java b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Point.java new file mode 100644 index 000000000..9bf9a3328 --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Point.java @@ -0,0 +1,30 @@ +package com.carffeine.carffeine.station.domain.station; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.math.BigDecimal; + +@ToString +@EqualsAndHashCode +@Getter +@AllArgsConstructor +public class Point { + + private final Latitude latitude; + private final Longitude longitude; + + public static Point of(long latitude, long longitude) { + return new Point(Latitude.from(BigDecimal.valueOf(latitude)), Longitude.from(BigDecimal.valueOf(longitude))); + } + + public static Point of(double latitude, double longitude) { + return new Point(Latitude.from(BigDecimal.valueOf(latitude)), Longitude.from(BigDecimal.valueOf(longitude))); + } + + public static Point of(BigDecimal latitude, BigDecimal longitude) { + return new Point(Latitude.from(latitude), Longitude.from(longitude)); + } +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Region.java b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Region.java index ce92391ab..6feb1f5f6 100644 --- a/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Region.java +++ b/backend/src/main/java/com/carffeine/carffeine/station/domain/station/Region.java @@ -7,13 +7,13 @@ public enum Region { SEOUL("서울특별시", BigDecimal.valueOf(37.540705), BigDecimal.valueOf(126.956764)), - INCHEON("인천광역시", BigDecimal.valueOf(37.469221), BigDecimal.valueOf(126.573234)), + INCHEON("인천광역시", BigDecimal.valueOf(37.3865), BigDecimal.valueOf(126.647)), GWANGJU("광주광역시", BigDecimal.valueOf(35.126033), BigDecimal.valueOf(126.831302)), DAEGU("대구광역시", BigDecimal.valueOf(35.798838), BigDecimal.valueOf(128.583052)), ULSAN("울산광역시", BigDecimal.valueOf(35.519301), BigDecimal.valueOf(129.239078)), DAEJEON("대전광역시", BigDecimal.valueOf(36.321655), BigDecimal.valueOf(127.378953)), BUSAN("부산광역시", BigDecimal.valueOf(35.198362), BigDecimal.valueOf(129.053922)), - GYEONGGI("경기도", BigDecimal.valueOf(37.567167), BigDecimal.valueOf(127.190292)), + GYEONGGI("경기도", BigDecimal.valueOf(37.2895), BigDecimal.valueOf(127.0535)), GANGWON("강원특별자치도", BigDecimal.valueOf(37.555837), BigDecimal.valueOf(128.209315)), CHUNGNAM("충청남도", BigDecimal.valueOf(36.557229), BigDecimal.valueOf(126.779757)), CHUNGBUK("충청북도", BigDecimal.valueOf(36.628503), BigDecimal.valueOf(127.929344)), diff --git a/backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/StationQueryRepository.java b/backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/StationQueryRepository.java index 7be4bc583..e92c37c8b 100644 --- a/backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/StationQueryRepository.java +++ b/backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/StationQueryRepository.java @@ -6,6 +6,7 @@ import com.carffeine.carffeine.station.infrastructure.repository.station.dto.ChargerSpecificResponse; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.RegionMarker; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationInfo; +import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationPoint; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSearchResult; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSimpleResponse; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSpecificResponse; @@ -221,4 +222,21 @@ private RegionMarker getFetch(Region region) { .where(station.address.startsWith(region.value())) .fetchOne(); } + + public List findStationPoints(int page, int size) { + return jpaQueryFactory.select(constructor(StationPoint.class, + station.latitude.value, + station.longitude.value + )) + .from(station) + .offset(size * page) + .limit(size) + .fetch(); + } + + public Long findStationCount() { + return jpaQueryFactory.select(station.stationId.count()) + .from(station) + .fetchOne(); + } } diff --git a/backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/dto/StationPoint.java b/backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/dto/StationPoint.java new file mode 100644 index 000000000..e26f13f92 --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/infrastructure/repository/station/dto/StationPoint.java @@ -0,0 +1,9 @@ +package com.carffeine.carffeine.station.infrastructure.repository.station.dto; + +import java.math.BigDecimal; + +public record StationPoint( + BigDecimal latitude, + BigDecimal longitude +) { +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridCacheService.java b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridCacheService.java new file mode 100644 index 000000000..f393a5fae --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridCacheService.java @@ -0,0 +1,75 @@ +package com.carffeine.carffeine.station.service.station; + +import com.carffeine.carffeine.station.domain.station.Coordinate; +import com.carffeine.carffeine.station.service.station.dto.ClusterRequest; +import com.carffeine.carffeine.station.service.station.dto.GridWithCount; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class StationGridCacheService { + + private static final int START_INDEX = 0; + private static final int NO_LATITUDE_FOUND = -1; + + private final List gridWithCounts; + + public void initialize(List gridWithCounts) { + this.gridWithCounts.addAll(gridWithCounts); + this.gridWithCounts.sort((o1, o2) -> { + int compare = o1.latitude().compareTo(o2.latitude()); + if (compare == 0) { + return o1.longitude().compareTo(o2.longitude()); + } + return compare; + }); + } + + public List findGrids(ClusterRequest request) { + Coordinate coordinate = Coordinate.of(request.latitude(), request.latitudeDelta(), request.longitude(), request.longitudeDelta()); + BigDecimal maxLatitude = coordinate.maxLatitudeValue(); + BigDecimal minLongitude = coordinate.minLongitudeValue(); + BigDecimal minLatitude = coordinate.minLatitudeValue(); + BigDecimal maxLongitude = coordinate.maxLongitudeValue(); + return findByCoordinate(minLatitude, maxLatitude, minLongitude, maxLongitude); + } + + private List findByCoordinate(BigDecimal minLatitude, BigDecimal maxLatitude, BigDecimal minLongitude, BigDecimal maxLongitude) { + int lowerBound = searchBoundaryLatitude(minLatitude, START_INDEX); + int upperBound = searchBoundaryLatitude(maxLatitude, lowerBound); + if (lowerBound == NO_LATITUDE_FOUND || upperBound == NO_LATITUDE_FOUND) { + return Collections.emptyList(); + } + return findBetweenLongitudes(minLongitude, maxLongitude, lowerBound, upperBound); + } + + private List findBetweenLongitudes(BigDecimal minLongitude, BigDecimal maxLongitude, int lowerBound, int upperBound) { + return gridWithCounts.stream() + .skip(lowerBound) + .limit(upperBound - lowerBound + 1) + .filter(grid -> grid.longitude().compareTo(minLongitude) >= 0 && grid.longitude().compareTo(maxLongitude) <= 0) + .toList(); + } + + private int searchBoundaryLatitude(BigDecimal latitude, int startIndex) { + int left = startIndex; + int right = gridWithCounts.size() - 1; + int result = -1; + while (left <= right) { + int middle = left + (right - left) / 2; + GridWithCount gridWithCount = gridWithCounts.get(middle); + if (gridWithCount.latitude().compareTo(latitude) >= 0) { + result = middle; + right = middle - 1; + } else { + left = middle + 1; + } + } + return result; + } +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridFacadeService.java b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridFacadeService.java new file mode 100644 index 000000000..850aa338a --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridFacadeService.java @@ -0,0 +1,66 @@ +package com.carffeine.carffeine.station.service.station; + +import com.carffeine.carffeine.station.domain.station.Coordinate; +import com.carffeine.carffeine.station.domain.station.Grid; +import com.carffeine.carffeine.station.domain.station.GridGenerator; +import com.carffeine.carffeine.station.domain.station.Point; +import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationPoint; +import com.carffeine.carffeine.station.service.station.dto.ClusterRequest; +import com.carffeine.carffeine.station.service.station.dto.GridWithCount; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class StationGridFacadeService { + + private final StationGridService stationGridService; + private final StationQueryService stationQueryService; + private final StationGridCacheService stationGridCacheService; + + public List createGridWithCounts() { + GridGenerator gridGenerator = new GridGenerator(); + List grids = gridGenerator.createKorea(); + int size = 1000; + int page = 0; + while (size == 1000) { + log.info("page : {}", page); + List stationPoint = stationQueryService.findStationPoint(page, size); + stationGridService.assignStationGrids(grids, stationPoint); + page++; + size = stationPoint.size(); + } + List list = grids.stream() + .filter(Grid::hasStation) + .map(it -> GridWithCount.createCenterPoint(it, it.stationSize())) + .toList(); + return new ArrayList<>(list); + } + + public List counts(ClusterRequest request) { + List gridWithCounts = stationGridCacheService.findGrids(request); + List displayGrid = createDisplayGrid(request); + List grids = stationGridService.assignStationGridsWithCount(displayGrid, gridWithCounts); + List list = grids.stream() + .filter(Grid::existsCount) + .map(it -> GridWithCount.createCenterPoint(it, it.getCount())) + .toList(); + return new ArrayList<>(list); + } + + private List createDisplayGrid(ClusterRequest request) { + GridGenerator gridGenerator = new GridGenerator(); + Coordinate coordinate = Coordinate.of(request.latitude(), request.latitudeDelta(), request.longitude(), request.longitudeDelta()); + BigDecimal maxLatitude = coordinate.maxLatitudeValue(); + BigDecimal minLongitude = coordinate.minLongitudeValue(); + BigDecimal minLatitude = coordinate.minLatitudeValue(); + BigDecimal maxLongitude = coordinate.maxLongitudeValue(); + return gridGenerator.create(Point.of(maxLatitude, minLongitude), Point.of(minLatitude, maxLongitude), request.latitudeDivisionSize(), request.longitudeDivisionSize()); + } +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridService.java b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridService.java new file mode 100644 index 000000000..5c34d6fc5 --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationGridService.java @@ -0,0 +1,45 @@ +package com.carffeine.carffeine.station.service.station; + +import com.carffeine.carffeine.station.domain.station.Grid; +import com.carffeine.carffeine.station.domain.station.Point; +import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationPoint; +import com.carffeine.carffeine.station.service.station.dto.GridWithCount; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class StationGridService { + + public List assignStationGrids(List grids, List stations) { + for (Grid grid : grids) { + for (StationPoint station : stations) { + addPointToGrid(grid, station); + } + } + return grids; + } + + private void addPointToGrid(Grid grid, StationPoint station) { + Point point = Point.of(station.latitude(), station.longitude()); + if (grid.isContain(point)) { + grid.addPoint(point); + } + } + + public List assignStationGridsWithCount(List grids, List gridWithCounts) { + for (Grid grid : grids) { + for (GridWithCount gridWithCount : gridWithCounts) { + addCountToGrid(grid, gridWithCount); + } + } + return grids; + } + + private void addCountToGrid(Grid grid, GridWithCount gridWithCount) { + Point point = Point.of(gridWithCount.latitude(), gridWithCount.longitude()); + if (grid.isContain(point)) { + grid.addCount(gridWithCount.count()); + } + } +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationQueryService.java b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationQueryService.java index 68ba8f01b..7a20ea077 100644 --- a/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationQueryService.java +++ b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationQueryService.java @@ -1,19 +1,20 @@ package com.carffeine.carffeine.station.service.station; +import com.carffeine.carffeine.city.infrastructure.repository.CityQueryRepository; +import com.carffeine.carffeine.city.infrastructure.repository.dto.CitySearchResponse; import com.carffeine.carffeine.station.domain.charger.ChargerType; import com.carffeine.carffeine.station.domain.station.Coordinate; import com.carffeine.carffeine.station.domain.station.Region; import com.carffeine.carffeine.station.exception.StationException; import com.carffeine.carffeine.station.exception.StationExceptionType; -import com.carffeine.carffeine.city.infrastructure.repository.CityQueryRepository; import com.carffeine.carffeine.station.infrastructure.repository.station.StationQueryRepository; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.RegionMarker; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationInfo; +import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationPoint; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSearchResult; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSimpleResponse; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSpecificResponse; import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationSummaryResponse; -import com.carffeine.carffeine.city.infrastructure.repository.dto.CitySearchResponse; import com.carffeine.carffeine.station.service.station.dto.CoordinateRequest; import com.carffeine.carffeine.station.service.station.dto.StationSearchResponse; import com.carffeine.carffeine.station.service.station.dto.StationsSearchResponse; @@ -99,4 +100,12 @@ public List findMarkersByRegions(List regionNames) { List regions = Region.regions(regionNames); return stationQueryRepository.findCountByRegions(regions); } + + public List findStationPoint(int page, int size) { + return stationQueryRepository.findStationPoints(page, size); + } + + public Long findStationCount() { + return stationQueryRepository.findStationCount(); + } } diff --git a/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationService.java b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationService.java index a9b388a1c..e650e8855 100644 --- a/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationService.java +++ b/backend/src/main/java/com/carffeine/carffeine/station/service/station/StationService.java @@ -8,7 +8,6 @@ import com.carffeine.carffeine.station.infrastructure.repository.charger.dto.ChargerStatusResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.time.DayOfWeek; @@ -26,7 +25,6 @@ public class StationService { private final PeriodicCongestionCustomRepository periodicCongestionCustomRepository; private final AtomicBoolean isRunning = new AtomicBoolean(false); - @Scheduled(cron = "0 0/10 * * * *") public void calculateCongestion() { if (isRunning.compareAndSet(false, true)) { LocalDateTime now = LocalDateTime.now(); diff --git a/backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/ClusterRequest.java b/backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/ClusterRequest.java new file mode 100644 index 000000000..5b7077028 --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/ClusterRequest.java @@ -0,0 +1,13 @@ +package com.carffeine.carffeine.station.service.station.dto; + +import java.math.BigDecimal; + +public record ClusterRequest( + BigDecimal latitude, + BigDecimal longitude, + BigDecimal latitudeDelta, + BigDecimal longitudeDelta, + int latitudeDivisionSize, + int longitudeDivisionSize +) { +} diff --git a/backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/GridWithCount.java b/backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/GridWithCount.java new file mode 100644 index 000000000..bdaae4481 --- /dev/null +++ b/backend/src/main/java/com/carffeine/carffeine/station/service/station/dto/GridWithCount.java @@ -0,0 +1,24 @@ +package com.carffeine.carffeine.station.service.station.dto; + +import com.carffeine.carffeine.station.domain.station.Grid; +import com.carffeine.carffeine.station.domain.station.Point; + +import java.math.BigDecimal; + +public record GridWithCount( + String id, + BigDecimal latitude, + BigDecimal longitude, + int count +) { + + public static GridWithCount createCenterPoint(Grid grid, int count) { + BigDecimal centerLatitude = grid.calculateCenterLatitude(); + BigDecimal centerLongitude = grid.calculateCenterLongitude(); + return new GridWithCount(grid.getId(), centerLatitude, centerLongitude, count); + } + + public static GridWithCount createRandom(Grid grid, int count, Point point) { + return new GridWithCount(grid.getId(), point.getLatitude().getValue(), point.getLongitude().getValue(), count); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 85b26241c..d6f9d4998 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -16,8 +16,7 @@ spring: active: local datasource: hikari: - minimum-idle: 22 - maximum-pool-size: 22 + maximum-pool-size: 5 task: scheduling: pool: diff --git a/backend/src/test/java/com/carffeine/carffeine/helper/MockBeanInjection.java b/backend/src/test/java/com/carffeine/carffeine/helper/MockBeanInjection.java index 9e5142192..c4e2d7bde 100644 --- a/backend/src/test/java/com/carffeine/carffeine/helper/MockBeanInjection.java +++ b/backend/src/test/java/com/carffeine/carffeine/helper/MockBeanInjection.java @@ -22,6 +22,7 @@ import com.carffeine.carffeine.station.service.review.ReplyService; import com.carffeine.carffeine.station.service.review.ReviewQueryService; import com.carffeine.carffeine.station.service.review.ReviewService; +import com.carffeine.carffeine.station.service.station.StationGridFacadeService; import com.carffeine.carffeine.station.service.station.StationQueryService; import com.carffeine.carffeine.station.service.station.StationService; import org.springframework.boot.test.mock.mockito.MockBean; @@ -78,4 +79,6 @@ public class MockBeanInjection { protected StationQueryService stationQueryService; @MockBean protected AdminCityService adminCityService; + @MockBean + protected StationGridFacadeService stationGridFacadeService; } diff --git a/backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridGeneratorTest.java b/backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridGeneratorTest.java new file mode 100644 index 000000000..140121cc2 --- /dev/null +++ b/backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridGeneratorTest.java @@ -0,0 +1,44 @@ +package com.carffeine.carffeine.station.domain.station; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class GridGeneratorTest { + + @Test + void 좌표_2개로_구역을_나눠준다() { + // given + GridGenerator gridGenerator = new GridGenerator(); + + // when + List grids = gridGenerator.create(Point.of(0, 3), Point.of(3, 0), 3, 3); + + // then + assertThat(grids).hasSize(9); + } + + @Test + void 우리나라를_세로_2Km_가로_2point5km면_570개의_그리드가_생성된다() { + // given + GridGenerator gridGenerator = new GridGenerator(); + + // when + List grids = gridGenerator.create(new Point(Latitude.from(BigDecimal.valueOf(38.6341)), Longitude.from(BigDecimal.valueOf(124.5377))), new Point(Latitude.from(BigDecimal.valueOf(33.1906)), Longitude.from(BigDecimal.valueOf(131.8795))), 19, 30); + + // then + assertSoftly(softly -> { + softly.assertThat(grids).hasSize(570); + softly.assertThat(grids.get(0).getTop()).isEqualTo(new Point(Latitude.from(BigDecimal.valueOf(33.1906)), Longitude.from(BigDecimal.valueOf(131.8795)))); + softly.assertThat(grids.get(0).getBottom()).isEqualTo(new Point(Latitude.from(BigDecimal.valueOf(33.4771)), Longitude.from(BigDecimal.valueOf(131.6348)))); + }); + } +} diff --git a/backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridTest.java b/backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridTest.java new file mode 100644 index 000000000..671c81d2b --- /dev/null +++ b/backend/src/test/java/com/carffeine/carffeine/station/domain/station/GridTest.java @@ -0,0 +1,51 @@ +package com.carffeine.carffeine.station.domain.station; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class GridTest { + + @Test + void 범위에_포함되면_true를_반환한다_top_down() { + // given + Grid grid = new Grid(Point.of(3, 0), Point.of(0, 3)); + Point point = Point.of(1, 1); + + // when + boolean result = grid.isContain(point); + + // then + assertThat(result).isTrue(); + } + + @Test + void 범위에_포함되면_true를_반환한다_down_top() { + // given + Grid grid = new Grid(Point.of(0, 3), Point.of(3, 0)); + Point point = Point.of(1, 1); + + // when + boolean result = grid.isContain(point); + + // then + assertThat(result).isTrue(); + } + + @Test + void 범위에_포함되지_않으면_false를_반환한다() { + // given + Grid grid = new Grid(Point.of(0, 0), Point.of(3, 3)); + Point point = Point.of(4, 5); + + // when + boolean result = grid.isContain(point); + + // then + assertThat(result).isFalse(); + } +} diff --git a/backend/src/test/java/com/carffeine/carffeine/station/domain/station/LatitudeTest.java b/backend/src/test/java/com/carffeine/carffeine/station/domain/station/LatitudeTest.java index f0bcd69d3..be21b7e94 100644 --- a/backend/src/test/java/com/carffeine/carffeine/station/domain/station/LatitudeTest.java +++ b/backend/src/test/java/com/carffeine/carffeine/station/domain/station/LatitudeTest.java @@ -1,29 +1,17 @@ package com.carffeine.carffeine.station.domain.station; -import com.carffeine.carffeine.station.exception.StationException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import java.math.BigDecimal; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class LatitudeTest { - @ValueSource(strings = {"32.9", "39.1"}) - @ParameterizedTest - void 대한민국_범위의_위도가_아니면_예외가_발생한다(String expect) { - assertThatThrownBy(() -> Latitude.from(expect)) - .isInstanceOf(StationException.class) - .hasMessage("유효하지 않는 위도입니다"); - } - @Test void 위도의_변화량에_따른_최소값을_구한다() { //given diff --git a/backend/src/test/java/com/carffeine/carffeine/station/service/station/StationGridServiceTest.java b/backend/src/test/java/com/carffeine/carffeine/station/service/station/StationGridServiceTest.java new file mode 100644 index 000000000..175899b3c --- /dev/null +++ b/backend/src/test/java/com/carffeine/carffeine/station/service/station/StationGridServiceTest.java @@ -0,0 +1,43 @@ +package com.carffeine.carffeine.station.service.station; + +import com.carffeine.carffeine.station.domain.station.Grid; +import com.carffeine.carffeine.station.domain.station.GridGenerator; +import com.carffeine.carffeine.station.domain.station.Point; +import com.carffeine.carffeine.station.infrastructure.repository.station.dto.StationPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class StationGridServiceTest { + + private StationGridService stationGridService; + + @BeforeEach + void setUp() { + stationGridService = new StationGridService(); + } + + @Test + void 그리드에_포함되는_충전소를_추가한다() { + // given + GridGenerator gridGenerator = new GridGenerator(); + List grids = gridGenerator.create(Point.of(39, 124), Point.of(38, 129), 3, 3); + StationPoint stationPoint = new StationPoint(BigDecimal.valueOf(38.3994933), BigDecimal.valueOf(128.3994933)); + StationPoint stationPoint2 = new StationPoint(BigDecimal.valueOf(36), BigDecimal.valueOf(123.3994933)); + + // when + List assignedGrids = stationGridService.assignStationGrids(grids, List.of(stationPoint, stationPoint2)); + + // then + assertThat(assignedGrids).map(it -> it.getPoints().size()) + .isEqualTo(List.of(1, 0, 0, 1, 0, 0, 1, 0, 0)); + } +}