diff --git a/app-scheduler/build.gradle b/app-scheduler/build.gradle index ef72720d..2dff959a 100644 --- a/app-scheduler/build.gradle +++ b/app-scheduler/build.gradle @@ -10,4 +10,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' // 프로메테우스 추가 implementation 'io.micrometer:micrometer-registry-prometheus' + + implementation group: 'org.hibernate.orm', name: 'hibernate-spatial', version: '6.3.1.Final' + + implementation 'org.springframework.boot:spring-boot-starter-aop' + + implementation 'org.springframework.retry:spring-retry:2.0.6' } diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/ApiCounter.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/ApiCounter.java new file mode 100644 index 00000000..1a7b2500 --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/ApiCounter.java @@ -0,0 +1,76 @@ +package com.parkingcomestrue.external.api; + +import java.util.concurrent.atomic.AtomicInteger; + +public class ApiCounter { + + private final int MIN_TOTAL_COUNT; + + private AtomicInteger totalCount; + private AtomicInteger errorCount; + private boolean isOpened; + + public ApiCounter() { + this.MIN_TOTAL_COUNT = 10; + this.totalCount = new AtomicInteger(0); + this.errorCount = new AtomicInteger(0); + this.isOpened = false; + } + + public ApiCounter(int minTotalCount) { + this.MIN_TOTAL_COUNT = minTotalCount; + this.totalCount = new AtomicInteger(0); + this.errorCount = new AtomicInteger(0); + this.isOpened = false; + } + + public void countUp() { + while (true) { + int expected = getTotalCount(); + int newValue = expected + 1; + if (totalCount.compareAndSet(expected, newValue)) { + return; + } + } + } + + public void errorCountUp() { + countUp(); + while (true) { + int expected = getErrorCount(); + int newValue = expected + 1; + if (errorCount.compareAndSet(expected, newValue)) { + return; + } + } + } + + public void reset() { + totalCount = new AtomicInteger(0); + errorCount = new AtomicInteger(0); + isOpened = false; + } + + public boolean isOpened() { + return isOpened; + } + + public void open() { + isOpened = true; + } + + public boolean isErrorRateOverThan(double errorRate) { + int currentTotalCount = getTotalCount(); + int currentErrorCount = getErrorCount(); + if (currentTotalCount < MIN_TOTAL_COUNT) { + return false; + } + double currentErrorRate = (double) currentErrorCount / currentTotalCount; + return currentErrorRate >= errorRate; + } + + public int getTotalCount() { + return totalCount.get(); + } + public int getErrorCount() { return errorCount.get(); } +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/AsyncApiExecutorConfig.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/AsyncApiExecutorConfig.java new file mode 100644 index 00000000..8b098751 --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/AsyncApiExecutorConfig.java @@ -0,0 +1,19 @@ +package com.parkingcomestrue.external.api; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AsyncApiExecutorConfig { + + @Bean + public ExecutorService executorService() { + return Executors.newFixedThreadPool(100, (Runnable r) -> { + Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + }); + } +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/CircuitBreaker.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/CircuitBreaker.java new file mode 100644 index 00000000..60f45343 --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/CircuitBreaker.java @@ -0,0 +1,17 @@ +package com.parkingcomestrue.external.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CircuitBreaker { + + int minTotalCount() default 10; + double errorRate() default 0.2; + long resetTime() default 30; + TimeUnit timeUnit() default TimeUnit.MINUTES; +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/CircuitBreakerAspect.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/CircuitBreakerAspect.java new file mode 100644 index 00000000..b22f600f --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/CircuitBreakerAspect.java @@ -0,0 +1,53 @@ +package com.parkingcomestrue.external.api; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Slf4j +@Aspect +@Component +public class CircuitBreakerAspect { + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10); + private final Map map = new ConcurrentHashMap<>(); + + @Around("@annotation(annotation)") + public Object around(ProceedingJoinPoint proceedingJoinPoint, CircuitBreaker annotation) { + ApiCounter apiCounter = getApiCounter(proceedingJoinPoint, annotation.minTotalCount()); + if (apiCounter.isOpened()) { + log.warn("현재 해당 {} API는 오류로 인해 중지되었습니다.", proceedingJoinPoint.getTarget()); + return null; + } + try { + Object result = proceedingJoinPoint.proceed(); + apiCounter.countUp(); + return result; + } catch (Throwable e) { + handleError(annotation, apiCounter); + return null; + } + } + + private ApiCounter getApiCounter(ProceedingJoinPoint proceedingJoinPoint, int minTotalCount) { + Object target = proceedingJoinPoint.getTarget(); + if (!map.containsKey(target)) { + map.put(target, new ApiCounter(minTotalCount)); + } + return map.get(target); + } + + private void handleError(CircuitBreaker annotation, ApiCounter apiCounter) { + apiCounter.errorCountUp(); + if (apiCounter.isErrorRateOverThan(annotation.errorRate())) { + apiCounter.open(); + scheduler.schedule(apiCounter::reset, annotation.resetTime(), annotation.timeUnit()); + } + } +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/HealthCheckResponse.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/HealthCheckResponse.java new file mode 100644 index 00000000..85da6b37 --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/HealthCheckResponse.java @@ -0,0 +1,15 @@ +package com.parkingcomestrue.external.api; + +import lombok.Getter; + +@Getter +public class HealthCheckResponse { + + boolean isHealthy; + int totalSize; + + public HealthCheckResponse(boolean isHealthy, int totalSize) { + this.isHealthy = isHealthy; + this.totalSize = totalSize; + } +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/HealthChecker.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/HealthChecker.java new file mode 100644 index 00000000..f1da52e5 --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/HealthChecker.java @@ -0,0 +1,6 @@ +package com.parkingcomestrue.external.api; + +public interface HealthChecker { + + HealthCheckResponse check(); +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/CoordinateApiService.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/CoordinateApiService.java similarity index 64% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/CoordinateApiService.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/CoordinateApiService.java index d1d8d3b3..3bc7ce1a 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/CoordinateApiService.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/CoordinateApiService.java @@ -1,8 +1,9 @@ -package com.parkingcomestrue.external.coordinate; +package com.parkingcomestrue.external.api.coordinate; import com.parkingcomestrue.common.domain.parking.Location; -import com.parkingcomestrue.external.coordinate.dto.CoordinateResponse; -import com.parkingcomestrue.external.coordinate.dto.CoordinateResponse.ExactLocation; +import com.parkingcomestrue.external.api.HealthChecker; +import com.parkingcomestrue.external.api.coordinate.dto.CoordinateResponse; +import com.parkingcomestrue.external.api.HealthCheckResponse; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -13,7 +14,7 @@ import org.springframework.web.util.UriComponentsBuilder; @Component -public class CoordinateApiService { +public class CoordinateApiService implements HealthChecker { private static final String KAKAO_URL = "https://dapi.kakao.com/v2/local/search/address.json"; @@ -32,12 +33,12 @@ public Location extractLocationByAddress(String address, Location location) { return location; } - ExactLocation exactLocation = getExactLocation(result); + CoordinateResponse.ExactLocation exactLocation = getExactLocation(result); return Location.of(exactLocation.getLongitude(), exactLocation.getLatitude()); } - private ExactLocation getExactLocation(ResponseEntity result) { - List exactLocations = result.getBody().getExactLocations(); + private CoordinateResponse.ExactLocation getExactLocation(ResponseEntity result) { + List exactLocations = result.getBody().getExactLocations(); return exactLocations.get(0); } @@ -59,4 +60,15 @@ private boolean isEmptyResultData(ResponseEntity result) { Integer matchingDataCount = result.getBody().getMeta().getTotalCount(); return matchingDataCount == 0; } + + @Override + public HealthCheckResponse check() { + UriComponents uriComponents = makeCompleteUri("health check"); + ResponseEntity response = connect(uriComponents); + return new HealthCheckResponse(isHealthy(response), 1); + } + + private boolean isHealthy(ResponseEntity response) { + return response.getStatusCode().is2xxSuccessful(); + } } diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/CoordinateErrorHandler.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/CoordinateErrorHandler.java similarity index 94% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/CoordinateErrorHandler.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/CoordinateErrorHandler.java index 83114fac..d94e2bb8 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/CoordinateErrorHandler.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/CoordinateErrorHandler.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.coordinate; +package com.parkingcomestrue.external.api.coordinate; import com.parkingcomestrue.external.support.exception.SchedulerException; import com.parkingcomestrue.external.support.exception.SchedulerExceptionInformation; diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/dto/CoordinateResponse.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/dto/CoordinateResponse.java similarity index 94% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/dto/CoordinateResponse.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/dto/CoordinateResponse.java index b91f2831..96555d8d 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/coordinate/dto/CoordinateResponse.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/coordinate/dto/CoordinateResponse.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.coordinate.dto; +package com.parkingcomestrue.external.api.coordinate.dto; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/ParkingApiService.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/ParkingApiService.java new file mode 100644 index 00000000..63229c52 --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/ParkingApiService.java @@ -0,0 +1,16 @@ +package com.parkingcomestrue.external.api.parkingapi; + +import com.parkingcomestrue.common.domain.parking.Parking; +import com.parkingcomestrue.external.api.HealthChecker; +import java.util.List; + +public interface ParkingApiService extends HealthChecker { + + default boolean offerCurrentParking() { + return false; + } + + List read(int pageNumber, int size); + + int getReadSize(); +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingAdapter.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingAdapter.java similarity index 96% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingAdapter.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingAdapter.java index ea0f3730..0be879b7 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingAdapter.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingAdapter.java @@ -1,4 +1,6 @@ -package com.parkingcomestrue.external.parkingapi.korea; +package com.parkingcomestrue.external.api.parkingapi.korea; + +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; import com.parkingcomestrue.common.domain.parking.BaseInformation; import com.parkingcomestrue.common.domain.parking.Fee; @@ -114,7 +116,7 @@ private TimeInfo toTimeInfo(String beginTime, String endTime) { private LocalTime parsingOperationTime(String time) { if (time.equals(HOURS_24)) { - return LocalTime.MAX; + return MAX_END_TIME; } try { return LocalTime.parse(time, TIME_FORMATTER); diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingApiService.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingApiService.java similarity index 57% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingApiService.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingApiService.java index 4ff9b3dc..876c6a05 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingApiService.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingApiService.java @@ -1,11 +1,11 @@ -package com.parkingcomestrue.external.parkingapi.korea; +package com.parkingcomestrue.external.api.parkingapi.korea; import com.parkingcomestrue.common.domain.parking.Parking; -import com.parkingcomestrue.external.parkingapi.ParkingApiService; +import com.parkingcomestrue.external.api.CircuitBreaker; +import com.parkingcomestrue.external.api.HealthCheckResponse; +import com.parkingcomestrue.external.api.parkingapi.ParkingApiService; import java.net.URI; -import java.util.HashSet; import java.util.List; -import java.util.Set; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; @@ -34,26 +34,20 @@ public KoreaParkingApiService(KoreaParkingAdapter adapter, } @Override - public List read() throws Exception { - Set result = new HashSet<>(); - for (int pageNumber = 1; ; pageNumber++) { - KoreaParkingResponse response = call(pageNumber, SIZE); - String resultCode = response.getResponse().getHeader().getResultCode(); - if (NORMAL_RESULT_CODE.equals(resultCode)) { - result.add(response); - continue; - } - break; - } - return result.stream() - .flatMap(response -> adapter.convert(response).stream()) - .toList(); + @CircuitBreaker + public List read(int pageNumber, int size) { + ResponseEntity response = call(pageNumber, size); + return adapter.convert(response.getBody()); } - private KoreaParkingResponse call(int startIndex, int size) { - URI uri = makeUri(startIndex, size); - ResponseEntity response = restTemplate.getForEntity(uri, KoreaParkingResponse.class); - return response.getBody(); + @Override + public int getReadSize() { + return SIZE; + } + + private ResponseEntity call(int pageNumber, int size) { + URI uri = makeUri(pageNumber, size); + return restTemplate.getForEntity(uri, KoreaParkingResponse.class); } private URI makeUri(int startIndex, int size) { @@ -66,4 +60,15 @@ private URI makeUri(int startIndex, int size) { .queryParam("type", RESULT_TYPE) .build(); } + + @Override + public HealthCheckResponse check() { + ResponseEntity response = call(1, 1); + return new HealthCheckResponse(isHealthy(response), response.getBody().getResponse().getBody().getTotalCount()); + } + + private boolean isHealthy(ResponseEntity response) { + return response.getStatusCode().is2xxSuccessful() && response.getBody().getResponse().getHeader() + .getResultCode().equals(NORMAL_RESULT_CODE); + } } diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingResponse.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingResponse.java similarity index 96% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingResponse.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingResponse.java index 5b926a49..e0632042 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingResponse.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingResponse.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.parkingapi.korea; +package com.parkingcomestrue.external.api.parkingapi.korea; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -29,6 +29,7 @@ public static class Header { public static class Body { private List items; + private int totalCount; @Getter @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingAdapter.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingAdapter.java similarity index 96% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingAdapter.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingAdapter.java index 70aa27c3..f3488f22 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingAdapter.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingAdapter.java @@ -1,4 +1,6 @@ -package com.parkingcomestrue.external.parkingapi.pusan; +package com.parkingcomestrue.external.api.parkingapi.pusan; + +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; import com.parkingcomestrue.common.domain.parking.BaseInformation; import com.parkingcomestrue.common.domain.parking.Fee; @@ -105,7 +107,7 @@ private TimeInfo toTimeInfo(String beginTime, String endTime) { private LocalTime parsingOperationTime(String time) { if (time.equals(HOURS_24)) { - return LocalTime.MAX; + return MAX_END_TIME; } try { return LocalTime.parse(time, TIME_FORMATTER); diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingApiService.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingApiService.java similarity index 58% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingApiService.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingApiService.java index 32162e0e..9a1c26b3 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingApiService.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingApiService.java @@ -1,7 +1,9 @@ -package com.parkingcomestrue.external.parkingapi.pusan; +package com.parkingcomestrue.external.api.parkingapi.pusan; import com.parkingcomestrue.common.domain.parking.Parking; -import com.parkingcomestrue.external.parkingapi.ParkingApiService; +import com.parkingcomestrue.external.api.CircuitBreaker; +import com.parkingcomestrue.external.api.HealthCheckResponse; +import com.parkingcomestrue.external.api.parkingapi.ParkingApiService; import java.net.URI; import java.util.List; import org.springframework.beans.factory.annotation.Qualifier; @@ -17,6 +19,7 @@ public class PusanPublicParkingApiService implements ParkingApiService { private static final String URL = "http://apis.data.go.kr/6260000/BusanPblcPrkngInfoService/getPblcPrkngInfo"; private static final String RESULT_TYPE = "json"; private static final int SIZE = 1000; + private static String NORMAL_CODE = "00"; @Value("${pusan-public-parking-key}") private String API_KEY; @@ -31,16 +34,15 @@ public PusanPublicParkingApiService(PusanPublicParkingAdapter adapter, } @Override - public List read() throws Exception { - PusanPublicParkingResponse response = call(1, SIZE); + @CircuitBreaker + public List read(int pageNumber, int size) { + PusanPublicParkingResponse response = call(pageNumber, size).getBody(); return adapter.convert(response); } - private PusanPublicParkingResponse call(int startIndex, int size) { - URI uri = makeUri(startIndex, size); - ResponseEntity response = restTemplate.getForEntity(uri, - PusanPublicParkingResponse.class); - return response.getBody(); + private ResponseEntity call(int pageNumber, int size) { + URI uri = makeUri(pageNumber, size); + return restTemplate.getForEntity(uri, PusanPublicParkingResponse.class); } private URI makeUri(int startIndex, int size) { @@ -58,4 +60,21 @@ private URI makeUri(int startIndex, int size) { public boolean offerCurrentParking() { return true; } + + @Override + public HealthCheckResponse check() { + ResponseEntity response = call(1, 1); + return new HealthCheckResponse(isHealthy(response), + response.getBody().getGetParkingInfoDetails().getBody().getTotalCount()); + } + + private boolean isHealthy(ResponseEntity response) { + return response.getStatusCode().is2xxSuccessful() && response.getBody().getGetParkingInfoDetails().getHeader() + .getResultCode().equals(NORMAL_CODE); + } + + @Override + public int getReadSize() { + return SIZE; + } } diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingResponse.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingResponse.java similarity index 89% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingResponse.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingResponse.java index 48d3142b..6c740247 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingResponse.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingResponse.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.parkingapi.pusan; +package com.parkingcomestrue.external.api.parkingapi.pusan; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -14,12 +14,21 @@ public class PusanPublicParkingResponse { @Getter @JsonIgnoreProperties(ignoreUnknown = true) public static class ParkingInfo { + + private Header header; private Body body; + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Header { + private String resultCode; + } + @Getter @JsonIgnoreProperties(ignoreUnknown = true) public static class Body { private Items items; + private int totalCount; } @Getter diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingAdapter.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingAdapter.java similarity index 97% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingAdapter.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingAdapter.java index 878760cf..cb93ed15 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingAdapter.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingAdapter.java @@ -1,4 +1,6 @@ -package com.parkingcomestrue.external.parkingapi.seoul; +package com.parkingcomestrue.external.api.parkingapi.seoul; + +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; import com.parkingcomestrue.common.domain.parking.BaseInformation; import com.parkingcomestrue.common.domain.parking.Fee; @@ -126,7 +128,7 @@ private OperatingTime getOperatingTime(final SeoulPublicParkingResponse.ParkingI private LocalTime parsingOperationTime(String time) { if (time.equals(HOURS_24)) { - return LocalTime.MAX; + return MAX_END_TIME; } try { return LocalTime.parse(time, TIME_FORMATTER); diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingApiService.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingApiService.java new file mode 100644 index 00000000..1a0c0dda --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingApiService.java @@ -0,0 +1,80 @@ +package com.parkingcomestrue.external.api.parkingapi.seoul; + +import com.parkingcomestrue.external.api.CircuitBreaker; +import com.parkingcomestrue.external.api.HealthCheckResponse; +import com.parkingcomestrue.external.api.parkingapi.ParkingApiService; +import com.parkingcomestrue.common.domain.parking.Parking; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +public class SeoulPublicParkingApiService implements ParkingApiService { + + private static final String URL = "http://openapi.seoul.go.kr:8088"; + private static final String API_NAME = "GetParkingInfo"; + private static final String RESULT_TYPE = "json"; + private static final int SIZE = 1000; + private static final String NORMAL_CODE = "INFO-000"; + + @Value("${seoul-public-parking-key}") + private String API_KEY; + + private final SeoulPublicParkingAdapter adapter; + private final RestTemplate restTemplate; + + public SeoulPublicParkingApiService(SeoulPublicParkingAdapter adapter, + @Qualifier("parkingApiRestTemplate") RestTemplate restTemplate) { + this.adapter = adapter; + this.restTemplate = restTemplate; + } + + @Override + @CircuitBreaker + public List read(int pageNumber, int size) { + int startIndex = (pageNumber - 1) * size + 1; + ResponseEntity response = call(startIndex, startIndex + size - 1); + return adapter.convert(response.getBody()); + } + + private ResponseEntity call(int startIndex, int lastIndex) { + URI uri = makeUri(startIndex, lastIndex); + return restTemplate.getForEntity(uri, SeoulPublicParkingResponse.class); + } + + private URI makeUri(int startIndex, int endIndex) { + return UriComponentsBuilder + .fromHttpUrl(URL) + .pathSegment(API_KEY, RESULT_TYPE, API_NAME, String.valueOf(startIndex), String.valueOf(endIndex)) + .encode(StandardCharsets.UTF_8) + .build() + .toUri(); + } + + @Override + public boolean offerCurrentParking() { + return true; + } + + @Override + public HealthCheckResponse check() { + ResponseEntity response = call(1, 1); + return new HealthCheckResponse(isHealthy(response), SIZE * 2); + } + + private boolean isHealthy(ResponseEntity response) { + return response.getStatusCode() + .is2xxSuccessful() && response.getBody().getParkingInfo().getResult().getCode().equals(NORMAL_CODE); + } + + @Override + public int getReadSize() { + return SIZE; + } +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingResponse.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingResponse.java similarity index 84% rename from app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingResponse.java rename to app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingResponse.java index 9bf9de06..425b7154 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingResponse.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingResponse.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.parkingapi.seoul; +package com.parkingcomestrue.external.api.parkingapi.seoul; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -17,9 +17,20 @@ public class SeoulPublicParkingResponse { @JsonIgnoreProperties(ignoreUnknown = true) public static class ParkingInfo { + @JsonProperty("RESULT") + private Result result; + @JsonProperty("row") private List rows; + @Getter + @JsonNaming(value = PropertyNamingStrategies.UpperSnakeCaseStrategy.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Result { + + private String code; + } + @Getter @JsonNaming(value = PropertyNamingStrategies.UpperSnakeCaseStrategy.class) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/config/RestTemplateConfig.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/config/RestTemplateConfig.java index e9d5f30f..7ffd1997 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/config/RestTemplateConfig.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/config/RestTemplateConfig.java @@ -1,6 +1,6 @@ package com.parkingcomestrue.external.config; -import com.parkingcomestrue.external.coordinate.CoordinateErrorHandler; +import com.parkingcomestrue.external.api.coordinate.CoordinateErrorHandler; import java.time.Duration; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -9,6 +9,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.RestTemplate; @Configuration @@ -21,18 +24,35 @@ public class RestTemplateConfig { public RestTemplate coordinateRestTemplate(RestTemplateBuilder restTemplateBuilder, @Value("${kakao.key}") String kakaoUrl) { return restTemplateBuilder + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(5)) .errorHandler(new CoordinateErrorHandler()) .defaultHeader(AUTH_HEADER, kakaoUrl) + .additionalInterceptors(clientHttpRequestInterceptor()) .build(); } + private ClientHttpRequestInterceptor clientHttpRequestInterceptor() { + return (request, body, execution) -> { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3)); + try { + return retryTemplate.execute(context -> execution.execute(request, body)); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + }; + } + @Bean @Qualifier("parkingApiRestTemplate") public RestTemplate parkingApiRestTemplate(RestTemplateBuilder restTemplateBuilder) { return restTemplateBuilder - .setConnectTimeout(Duration.ofSeconds(30)) + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(60)) .errorHandler(new ParkingApiErrorHandler()) .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_UTF8_VALUE) + .additionalInterceptors(clientHttpRequestInterceptor()) .build(); } } diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/ParkingApiService.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/ParkingApiService.java deleted file mode 100644 index 877d6a69..00000000 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/ParkingApiService.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.parkingcomestrue.external.parkingapi; - -import com.parkingcomestrue.common.domain.parking.Parking; -import java.util.List; - -public interface ParkingApiService { - - default boolean offerCurrentParking() { - return false; - } - - List read() throws Exception; -} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingApiService.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingApiService.java deleted file mode 100644 index c009bc94..00000000 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingApiService.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.parkingcomestrue.external.parkingapi.seoul; - -import com.parkingcomestrue.external.parkingapi.ParkingApiService; -import com.parkingcomestrue.common.domain.parking.Parking; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; - -@Component -public class SeoulPublicParkingApiService implements ParkingApiService { - - private static final String URL = "http://openapi.seoul.go.kr:8088"; - private static final String API_NAME = "GetParkingInfo"; - private static final String RESULT_TYPE = "json"; - private static final int SIZE = 1000; - - @Value("${seoul-public-parking-key}") - private String API_KEY; - - private final SeoulPublicParkingAdapter adapter; - private final RestTemplate restTemplate; - - public SeoulPublicParkingApiService(SeoulPublicParkingAdapter adapter, - @Qualifier("parkingApiRestTemplate") RestTemplate restTemplate) { - this.adapter = adapter; - this.restTemplate = restTemplate; - } - - @Override - public List read() throws Exception { - List response = call(); - return response.stream() - .map(adapter::convert) - .flatMap(Collection::stream) - .toList(); - } - - private List call() { - List result = new LinkedList<>(); - for (int i = 0; i < 2 * SIZE; i += SIZE) { - URI uri = makeUri(String.valueOf(i), String.valueOf(i + SIZE - 1)); - - ResponseEntity response = restTemplate.getForEntity(uri, - SeoulPublicParkingResponse.class); - result.add(response.getBody()); - } - return Collections.unmodifiableList(result); - } - - private URI makeUri(String startIndex, String endIndex) { - return UriComponentsBuilder - .fromHttpUrl(URL) - .pathSegment(API_KEY, RESULT_TYPE, API_NAME, startIndex, endIndex) - .encode(StandardCharsets.UTF_8) - .build() - .toUri(); - } - - @Override - public boolean offerCurrentParking() { - return true; - } -} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBatchRepository.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBatchRepository.java new file mode 100644 index 00000000..6bccfbca --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBatchRepository.java @@ -0,0 +1,10 @@ +package com.parkingcomestrue.external.respository; + +import com.parkingcomestrue.common.domain.parking.Parking; +import com.parkingcomestrue.common.domain.parking.repository.ParkingRepository; +import java.util.List; + +public interface ParkingBatchRepository extends ParkingRepository { + + void saveWithBatch(List parkingLots); +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBatchRepositoryImpl.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBatchRepositoryImpl.java new file mode 100644 index 00000000..a2546cde --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBatchRepositoryImpl.java @@ -0,0 +1,54 @@ +package com.parkingcomestrue.external.respository; + +import com.parkingcomestrue.common.domain.parking.Parking; +import com.parkingcomestrue.common.domain.parking.repository.ParkingRepository; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Point; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class ParkingBatchRepositoryImpl implements ParkingBatchRepository { + + private final int BATCH_SIZE = 2000; + + private final ParkingBulkRepository parkingBulkRepository; + private final ParkingRepository parkingRepository; + + @Override + public void saveWithBatch(List parkingLots) { + for (int i = 0; i < parkingLots.size(); i += BATCH_SIZE) { + int end = Math.min(i + BATCH_SIZE, parkingLots.size()); + List subParkingLots = parkingLots.subList(i, end); + parkingBulkRepository.saveAllWithBulk(subParkingLots); + } + } + + @Override + public Optional findById(Long id) { + return parkingRepository.findById(id); + } + + @Override + public Set findAllByBaseInformationNameIn(Set parkingNames) { + return parkingRepository.findAllByBaseInformationNameIn(parkingNames); + } + + @Override + public void saveAll(Iterable parkingLots) { + parkingRepository.saveAll(parkingLots); + } + + @Override + public List findAroundParkingLotsOrderByDistance(Point point, int radius) { + return parkingRepository.findAroundParkingLotsOrderByDistance(point, radius); + } + + @Override + public List findAroundParkingLots(Point point, int radius) { + return parkingRepository.findAroundParkingLots(point, radius); + } +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBulkRepository.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBulkRepository.java new file mode 100644 index 00000000..bdd97795 --- /dev/null +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/respository/ParkingBulkRepository.java @@ -0,0 +1,74 @@ +package com.parkingcomestrue.external.respository; + +import com.parkingcomestrue.common.domain.parking.Location; +import com.parkingcomestrue.common.domain.parking.Parking; +import java.sql.PreparedStatement; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Repository +public class ParkingBulkRepository { + + private final ParameterizedPreparedStatementSetter PARKING_PARAMETERIZED_PREPARED_STATEMENT_SETTER = (PreparedStatement ps, Parking parking) -> { + ps.setInt(1, parking.getFeePolicy().getBaseFee().getFee()); + ps.setInt(2, parking.getFeePolicy().getBaseTimeUnit().getTimeUnit()); + ps.setInt(3, parking.getFeePolicy().getExtraFee().getFee()); + ps.setInt(4, parking.getFeePolicy().getExtraTimeUnit().getTimeUnit()); + ps.setInt(5, parking.getFeePolicy().getDayMaximumFee().getFee()); + + ps.setInt(6, parking.getSpace().getCapacity()); + ps.setInt(7, parking.getSpace().getCurrentParking()); + + ps.setString(8, parking.getOperatingTime().getHolidayOperatingTime().toString()); + ps.setString(9, parking.getFreeOperatingTime().getHolidayFreeOperatingTime().toString()); + + ps.setString(10, parking.getOperatingTime().getSaturdayOperatingTime().toString()); + ps.setString(11, parking.getFreeOperatingTime().getSaturdayFreeOperatingTime().toString()); + + ps.setString(12, parking.getOperatingTime().getWeekdayOperatingTime().toString()); + ps.setString(13, parking.getFreeOperatingTime().getWeekdayFreeOperatingTime().toString()); + + ps.setObject(14, parking.getCreatedAt()); + ps.setObject(15, parking.getUpdatedAt()); + + ps.setString(16, parking.getBaseInformation().getAddress()); + ps.setString(17, parking.getBaseInformation().getName()); + ps.setString(18, parking.getBaseInformation().getTel()); + ps.setString(19, parking.getBaseInformation().getOperationType().name()); + ps.setString(20, parking.getBaseInformation().getParkingType().name()); + ps.setString(21, parking.getBaseInformation().getPayTypesName()); + ps.setString(22, toWKT(parking.getLocation())); + }; + + private String toWKT(Location location) { + return "POINT(" + location.getLatitude() + " " + location.getLongitude() + ")"; + } + + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void saveAllWithBulk(List parkingLots) { + String sql = "INSERT INTO parking " + + "(base_fee, base_time_unit, extra_fee, extra_time_unit, day_maximum_fee, " + + "capacity, current_parking, " + + "holiday_operating_time, holiday_free_operating_time, " + + "saturday_operating_time, saturday_free_operating_time, " + + "weekday_operating_time, weekday_free_operating_time, " + + "created_at, updated_at, " + + "address, name, tel, operation_type, parking_type, pay_types, location) " + + "VALUES " + + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ST_GeomFromText(?, 4326))"; + + jdbcTemplate.batchUpdate( + sql, + parkingLots, + parkingLots.size(), + PARKING_PARAMETERIZED_PREPARED_STATEMENT_SETTER + ); + } +} diff --git a/app-scheduler/src/main/java/com/parkingcomestrue/external/scheduler/ParkingUpdateScheduler.java b/app-scheduler/src/main/java/com/parkingcomestrue/external/scheduler/ParkingUpdateScheduler.java index 221d473a..633ff684 100644 --- a/app-scheduler/src/main/java/com/parkingcomestrue/external/scheduler/ParkingUpdateScheduler.java +++ b/app-scheduler/src/main/java/com/parkingcomestrue/external/scheduler/ParkingUpdateScheduler.java @@ -2,20 +2,23 @@ import com.parkingcomestrue.common.domain.parking.Location; import com.parkingcomestrue.common.domain.parking.Parking; -import com.parkingcomestrue.common.domain.parking.repository.ParkingRepository; -import com.parkingcomestrue.external.coordinate.CoordinateApiService; -import com.parkingcomestrue.external.parkingapi.ParkingApiService; -import com.parkingcomestrue.external.service.ParkingService; +import com.parkingcomestrue.external.api.coordinate.CoordinateApiService; +import com.parkingcomestrue.external.api.HealthCheckResponse; +import com.parkingcomestrue.external.api.parkingapi.ParkingApiService; +import com.parkingcomestrue.external.respository.ParkingBatchRepository; import java.util.Collection; -import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; @@ -28,32 +31,63 @@ public class ParkingUpdateScheduler { private final List parkingApiServices; private final CoordinateApiService coordinateApiService; - private final ParkingRepository parkingRepository; - private final ParkingService parkingService; + private final ParkingBatchRepository parkingBatchRepository; + private final ExecutorService executorService; @Scheduled(cron = "0 */30 * * * *") public void autoUpdateOfferCurrentParking() { Map parkingLots = readBy(ParkingApiService::offerCurrentParking); Map saved = findAllByName(parkingLots.keySet()); - List newParkingLots = findNewParkingLots(parkingLots, saved); - parkingService.updateParkingLots(parkingLots, saved, newParkingLots); + updateSavedParkingLots(parkingLots, saved); + saveNewParkingLots(parkingLots, saved); } private Map readBy(Predicate currentParkingAvailable) { + List parkingApis = filterBy(currentParkingAvailable); + Map result = new HashMap<>(); + for (ParkingApiService parkingApi : parkingApis) { + HealthCheckResponse healthCheckResponse = parkingApi.check(); + if (healthCheckResponse.isHealthy()) { + List>> responses = fetchParkingDataAsync( + parkingApi, healthCheckResponse.getTotalSize()); + Map response = collectParkingData(responses); + result.putAll(response); + } + } + return result; + } + + private List filterBy(Predicate currentParkingAvailable) { return parkingApiServices.stream() .filter(currentParkingAvailable) - .map(this::read) - .flatMap(Collection::stream) - .collect(toParkingMap()); + .toList(); + } + + private List>> fetchParkingDataAsync(ParkingApiService parkingApi, int totalSize) { + int readSize = parkingApi.getReadSize(); + int lastPageNumber = calculateLastPageNumber(totalSize, readSize); + + return Stream.iterate(1, i -> i <= lastPageNumber, i -> i + 1) + .map(i -> CompletableFuture.supplyAsync(() -> parkingApi.read(i, readSize), executorService)) + .toList(); } - private List read(ParkingApiService parkingApiService) { - try { - return parkingApiService.read(); - } catch (Exception e) { - log.warn("Error while converting {} to Parking {}", parkingApiService.getClass(), e.getMessage()); - return Collections.emptyList(); + private int calculateLastPageNumber(int totalSize, int readSize) { + int lastPageNumber = totalSize / readSize; + if (totalSize % readSize == 0) { + return lastPageNumber; } + return lastPageNumber + 1; + } + + private Map collectParkingData(List>> responses) { + List> parkingLots = responses.stream() + .map(CompletableFuture::join) + .toList(); + + return parkingLots.stream() + .flatMap(Collection::stream) + .collect(toParkingMap()); } private Collector> toParkingMap() { @@ -65,25 +99,32 @@ private List read(ParkingApiService parkingApiService) { } private Map findAllByName(Set names) { - return parkingRepository.findAllByBaseInformationNameIn(names) + return parkingBatchRepository.findAllByBaseInformationNameIn(names) .stream() .collect(toParkingMap()); } - private List findNewParkingLots(Map parkingLots, Map saved) { + private void updateSavedParkingLots(Map parkingLots, Map saved) { + for (String parkingName : saved.keySet()) { + Parking origin = saved.get(parkingName); + Parking updated = parkingLots.get(parkingName); + origin.update(updated); + } + } + + private void saveNewParkingLots(Map parkingLots, Map saved) { List newParkingLots = parkingLots.keySet() .stream() .filter(parkingName -> !saved.containsKey(parkingName)) .map(parkingLots::get) .toList(); updateLocation(newParkingLots); - return newParkingLots; + parkingBatchRepository.saveWithBatch(newParkingLots); } - private void updateLocation(List newParkingLots) { for (Parking parking : newParkingLots) { - if (!parking.getLocation().equals(Location.NO_PROVIDE)) { + if (parking.isLocationAvailable()) { continue; } Location locationByAddress = coordinateApiService.extractLocationByAddress( @@ -97,7 +138,7 @@ private void updateLocation(List newParkingLots) { public void autoUpdateNotOfferCurrentParking() { Map parkingLots = readBy(parkingApiService -> !parkingApiService.offerCurrentParking()); Map saved = findAllByName(parkingLots.keySet()); - List newParkingLots = findNewParkingLots(parkingLots, saved); - parkingService.updateParkingLots(parkingLots, saved, newParkingLots); + updateSavedParkingLots(parkingLots, saved); + saveNewParkingLots(parkingLots, saved); } } diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/api/ApiCounterTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/ApiCounterTest.java new file mode 100644 index 00000000..b26bd30d --- /dev/null +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/ApiCounterTest.java @@ -0,0 +1,91 @@ +package com.parkingcomestrue.external.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; + +class ApiCounterTest { + + @Test + void 전체_요청이_최소_횟수를_넘고_예외가_특정_지수를_넘어가면_true를_반환한다() { + //given + ApiCounter apiCounter = new ApiCounter(); + + for (int i = 0; i < 8; i++) { + apiCounter.countUp(); + } + for (int i = 0; i < 2; i++) { + apiCounter.errorCountUp(); + } + + //when + boolean actual = apiCounter.isErrorRateOverThan(0.2); + + //then + assertThat(actual).isTrue(); + } + + @Test + void 여러_스레드에서도_전체_요청이_최소_횟수를_넘고_예외가_특정_지수를_넘어가면_true를_반환한다() throws InterruptedException { + //given + ExecutorService executorService = Executors.newFixedThreadPool(30); + ApiCounter apiCounter = new ApiCounter(); + int threadCount = 1000; + CountDownLatch latch = new CountDownLatch(threadCount); + + //when + for (int i = 0; i < threadCount; i++) { + if (i % 10 == 0 || i % 10 == 1) { + executorService.submit(() -> { + try { + apiCounter.errorCountUp(); + } finally { + latch.countDown(); + } + }); + continue; + } + executorService.submit(() -> { + try { + apiCounter.countUp(); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + boolean actual = apiCounter.isErrorRateOverThan(0.2); + + //then + assertThat(actual).isTrue(); + } + + @Test + void 여러_스레드에서_카운트를_증가시킬수있다() throws InterruptedException { + //given + ExecutorService executorService = Executors.newFixedThreadPool(30); + ApiCounter apiCounter = new ApiCounter(); + int threadCount = 1000; + CountDownLatch latch = new CountDownLatch(threadCount); + + //when + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + apiCounter.countUp(); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + + //then + assertThat(apiCounter.getTotalCount()).isEqualTo(threadCount); + } +} diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/api/AsyncApiExecutorConfigTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/AsyncApiExecutorConfigTest.java new file mode 100644 index 00000000..5547394b --- /dev/null +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/AsyncApiExecutorConfigTest.java @@ -0,0 +1,57 @@ +package com.parkingcomestrue.external.api; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +class AsyncApiExecutorConfigTest { + + private static final int MINUTE = 1000; + + private final ExecutorService executorService = Executors.newFixedThreadPool(100, (Runnable r) -> { + Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + }); + + @Test + void executeAsync_메서드를_사용하면_100개의_스레드로_비동기_동작한다() { + //given + int pageNumber = 1; + int lastPageNumber = 100; + + //when + long start = System.currentTimeMillis(); + List> testCalls = Stream.iterate(pageNumber, i -> i <= lastPageNumber, i -> i + 1) + .map(i -> CompletableFuture.supplyAsync(() -> testCall(i), executorService)) + .toList(); + long end = System.currentTimeMillis(); + + Integer sum = CompletableFuture.allOf(testCalls.toArray(new CompletableFuture[0])) + .thenApply(Void -> testCalls.stream() + .mapToInt(CompletableFuture::join) + .sum()) + .join(); + + Integer expected = lastPageNumber * (lastPageNumber + 1) / 2; // 가우스 합 + + //then + SoftAssertions.assertSoftly(soft -> { + soft.assertThat((end - start)).isLessThanOrEqualTo(100 * MINUTE); + soft.assertThat(sum).isEqualTo(expected); + }); + } + + private Integer testCall(Integer pageNumber) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return pageNumber; + } +} diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/api/CircuitBreakerAspectTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/CircuitBreakerAspectTest.java new file mode 100644 index 00000000..a82fc94e --- /dev/null +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/CircuitBreakerAspectTest.java @@ -0,0 +1,62 @@ +package com.parkingcomestrue.external.api; + +import com.parkingcomestrue.fake.CircuitBreakerTestService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CircuitBreakerAspectTest { + + /** + * 요청 중 20%의 예외가 발생하면 api 요청 잠김 + * 잠긴 후, 2초 후에 다시 요청보내지도록 reset + */ + @Autowired + private CircuitBreakerTestService service; + + private boolean[] isExecuted = {false, false}; + + @Test + void 서비스에_에러가_특정_지수를_넘으면_요청이_잠긴다() { + //given + for (int i = 0; i < 8; i++) { + service.call(() -> {}); + } + for (int i = 0; i < 2; i++) { + service.call(() -> {throw new RuntimeException();}); + } + + //when + service.call(() -> isExecuted[0] = true); + + //then + Assertions.assertThat(isExecuted[0]).isFalse(); + } + + @Test + void 서비스가_잠긴후_특정시간이_지나면_다시_요청을_보낼수있다() throws ExecutionException, InterruptedException { + //given + for (int i = 0; i < 8; i++) { + service.call(() -> {}); + } + for (int i = 0; i < 2; i++) { + service.call(() -> {throw new RuntimeException();}); + } + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + //when + ScheduledFuture future = scheduler.schedule(() -> service.call(() -> isExecuted[1] = true), 2, + TimeUnit.SECONDS); + future.get(); + + //then + Assertions.assertThat(isExecuted[1]).isTrue(); + } +} diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/coordinate/CoordinateApiServiceTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/coordinate/CoordinateApiServiceTest.java similarity index 92% rename from app-scheduler/src/test/java/com/parkingcomestrue/external/coordinate/CoordinateApiServiceTest.java rename to app-scheduler/src/test/java/com/parkingcomestrue/external/api/coordinate/CoordinateApiServiceTest.java index e1cb42a9..52528d76 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/external/coordinate/CoordinateApiServiceTest.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/coordinate/CoordinateApiServiceTest.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.coordinate; +package com.parkingcomestrue.external.api.coordinate; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -6,7 +6,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; -import com.parkingcomestrue.external.coordinate.dto.CoordinateResponse; +import com.parkingcomestrue.external.api.coordinate.dto.CoordinateResponse; import com.parkingcomestrue.common.domain.parking.Location; import java.util.Collections; import org.junit.jupiter.api.Test; diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingAdapterTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingAdapterTest.java similarity index 94% rename from app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingAdapterTest.java rename to app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingAdapterTest.java index 4cdd1d80..ca5325da 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/korea/KoreaParkingAdapterTest.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/korea/KoreaParkingAdapterTest.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.parkingapi.korea; +package com.parkingcomestrue.external.api.parkingapi.korea; import com.fasterxml.jackson.databind.ObjectMapper; import com.parkingcomestrue.common.domain.parking.Fee; @@ -75,9 +75,9 @@ class KoreaParkingAdapterTest { soft.assertThat(parking.getFeePolicy().getExtraTimeUnit()).isEqualTo(TimeUnit.NO_INFO); soft.assertThat(parking.getFeePolicy().getDayMaximumFee()).isEqualTo(Fee.NO_INFO); - soft.assertThat(parking.getOperatingTime().getWeekday()).isEqualTo(TimeInfo.ALL_DAY); - soft.assertThat(parking.getOperatingTime().getSaturday()).isEqualTo(TimeInfo.ALL_DAY); - soft.assertThat(parking.getOperatingTime().getHoliday()).isEqualTo(TimeInfo.ALL_DAY); + soft.assertThat(parking.getOperatingTime().getWeekdayOperatingTime()).isEqualTo(TimeInfo.ALL_DAY); + soft.assertThat(parking.getOperatingTime().getSaturdayOperatingTime()).isEqualTo(TimeInfo.ALL_DAY); + soft.assertThat(parking.getOperatingTime().getHolidayOperatingTime()).isEqualTo(TimeInfo.ALL_DAY); soft.assertThat(parking.getBaseInformation().getPayTypesDescription()).isEqualTo(PayType.CARD.getDescription()); } diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingAdapterTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingAdapterTest.java similarity index 94% rename from app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingAdapterTest.java rename to app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingAdapterTest.java index e3120536..317cbae1 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/pusan/PusanPublicParkingAdapterTest.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/pusan/PusanPublicParkingAdapterTest.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.parkingapi.pusan; +package com.parkingcomestrue.external.api.parkingapi.pusan; import com.fasterxml.jackson.databind.ObjectMapper; import com.parkingcomestrue.common.domain.parking.Fee; @@ -76,9 +76,9 @@ class PusanPublicParkingAdapterTest { soft.assertThat(parking.getFeePolicy().getExtraTimeUnit()).isEqualTo(TimeUnit.from(10)); soft.assertThat(parking.getFeePolicy().getDayMaximumFee()).isEqualTo(Fee.ZERO); - soft.assertThat(parking.getOperatingTime().getWeekday()).isEqualTo(TimeInfo.ALL_DAY); - soft.assertThat(parking.getOperatingTime().getSaturday()).isEqualTo(TimeInfo.ALL_DAY); - soft.assertThat(parking.getOperatingTime().getHoliday()).isEqualTo(TimeInfo.ALL_DAY); + soft.assertThat(parking.getOperatingTime().getWeekdayOperatingTime()).isEqualTo(TimeInfo.ALL_DAY); + soft.assertThat(parking.getOperatingTime().getSaturdayOperatingTime()).isEqualTo(TimeInfo.ALL_DAY); + soft.assertThat(parking.getOperatingTime().getHolidayOperatingTime()).isEqualTo(TimeInfo.ALL_DAY); soft.assertThat(parking.getBaseInformation().getPayTypesDescription()).isEqualTo(PayType.NO_INFO.getDescription()); } diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingAdapterTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingAdapterTest.java similarity index 92% rename from app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingAdapterTest.java rename to app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingAdapterTest.java index 075fa109..fa49f365 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/external/parkingapi/seoul/SeoulPublicParkingAdapterTest.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/api/parkingapi/seoul/SeoulPublicParkingAdapterTest.java @@ -1,4 +1,4 @@ -package com.parkingcomestrue.external.parkingapi.seoul; +package com.parkingcomestrue.external.api.parkingapi.seoul; import com.fasterxml.jackson.databind.ObjectMapper; import com.parkingcomestrue.common.domain.parking.Fee; @@ -76,9 +76,9 @@ class SeoulPublicParkingAdapterTest { soft.assertThat(parking.getFeePolicy().getExtraTimeUnit()).isEqualTo(TimeUnit.from(5)); soft.assertThat(parking.getFeePolicy().getDayMaximumFee()).isEqualTo(Fee.ZERO); - soft.assertThat(parking.getOperatingTime().getWeekday()).isEqualTo(new TimeInfo(LocalTime.of(9, 0), LocalTime.of(19, 0))); - soft.assertThat(parking.getOperatingTime().getSaturday()).isEqualTo(new TimeInfo(LocalTime.of(9, 0), LocalTime.of(15, 0))); - soft.assertThat(parking.getOperatingTime().getHoliday()).isEqualTo(new TimeInfo(LocalTime.of(0, 0), LocalTime.of(0, 0))); + soft.assertThat(parking.getOperatingTime().getWeekdayOperatingTime()).isEqualTo(new TimeInfo(LocalTime.of(9, 0), LocalTime.of(19, 0))); + soft.assertThat(parking.getOperatingTime().getSaturdayOperatingTime()).isEqualTo(new TimeInfo(LocalTime.of(9, 0), LocalTime.of(15, 0))); + soft.assertThat(parking.getOperatingTime().getHolidayOperatingTime()).isEqualTo(new TimeInfo(LocalTime.of(0, 0), LocalTime.of(0, 0))); soft.assertThat(parking.getBaseInformation().getPayTypesDescription()).isEqualTo(PayType.NO_INFO.getDescription()); } diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/external/scheduler/ParkingUpdateSchedulerTest.java b/app-scheduler/src/test/java/com/parkingcomestrue/external/scheduler/ParkingUpdateSchedulerTest.java index f7e13b8a..1082c547 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/external/scheduler/ParkingUpdateSchedulerTest.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/external/scheduler/ParkingUpdateSchedulerTest.java @@ -1,29 +1,35 @@ package com.parkingcomestrue.external.scheduler; -import com.parkingcomestrue.external.coordinate.CoordinateApiService; -import com.parkingcomestrue.external.service.ParkingService; +import com.parkingcomestrue.external.api.coordinate.CoordinateApiService; import com.parkingcomestrue.fake.ExceptionParkingApiService; import com.parkingcomestrue.fake.FakeCoordinateApiService; +import com.parkingcomestrue.fake.FakeParkingBatchRepository; import com.parkingcomestrue.fake.NotOfferCurrentParkingApiService; import com.parkingcomestrue.fake.OfferCurrentParkingApiService; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import repository.BasicParkingRepository; class ParkingUpdateSchedulerTest { - private final BasicParkingRepository parkingRepository = new BasicParkingRepository(); + private final FakeParkingBatchRepository parkingRepository = new FakeParkingBatchRepository(); private final CoordinateApiService coordinateService = new FakeCoordinateApiService(); + private final ExecutorService executorService = Executors.newFixedThreadPool(100, (Runnable r) -> { + Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + }); @DisplayName("실시간 주차 대수를 제공하는 API에서 주차장이 0~4까지 저장되어 있는 상태에서 0~9까지 주차장을 읽어와 업데이트한다.") @Test void autoUpdateOfferCurrentParking() { //given OfferCurrentParkingApiService offerCurrentParkingApiService = new OfferCurrentParkingApiService(5); - parkingRepository.saveAll(offerCurrentParkingApiService.read()); + parkingRepository.saveAll(offerCurrentParkingApiService.read(0, offerCurrentParkingApiService.getReadSize())); int readSize = 10; offerCurrentParkingApiService.setReadSize(readSize); @@ -31,7 +37,7 @@ void autoUpdateOfferCurrentParking() { List.of(offerCurrentParkingApiService), coordinateService, parkingRepository, - new ParkingService(parkingRepository) + executorService ); //when @@ -47,7 +53,7 @@ void autoUpdateNotOfferCurrentParking() { //given NotOfferCurrentParkingApiService notOfferCurrentParkingApiService = new NotOfferCurrentParkingApiService( 5); - parkingRepository.saveAll(notOfferCurrentParkingApiService.read()); + parkingRepository.saveAll(notOfferCurrentParkingApiService.read(0, notOfferCurrentParkingApiService.getReadSize())); int readSize = 10; notOfferCurrentParkingApiService.setReadSize(readSize); @@ -55,7 +61,7 @@ void autoUpdateNotOfferCurrentParking() { List.of(notOfferCurrentParkingApiService), coordinateService, parkingRepository, - new ParkingService(parkingRepository) + executorService ); //when @@ -72,7 +78,7 @@ void notAffectBetweenOfferAndNotOfferCurrentParking() { OfferCurrentParkingApiService offerCurrentParkingApiService = new OfferCurrentParkingApiService(5); NotOfferCurrentParkingApiService notOfferCurrentParkingApiService = new NotOfferCurrentParkingApiService( 5); - parkingRepository.saveAll(offerCurrentParkingApiService.read()); + parkingRepository.saveAll(offerCurrentParkingApiService.read(0, offerCurrentParkingApiService.getReadSize())); int readSize = 10; notOfferCurrentParkingApiService.setReadSize(readSize); @@ -80,7 +86,7 @@ void notAffectBetweenOfferAndNotOfferCurrentParking() { List.of(offerCurrentParkingApiService, notOfferCurrentParkingApiService), coordinateService, parkingRepository, - new ParkingService(parkingRepository) + executorService ); //when @@ -98,7 +104,7 @@ void autoUpdateWithExceptionApi() { List.of(new OfferCurrentParkingApiService(5), new ExceptionParkingApiService()), coordinateService, parkingRepository, - new ParkingService(parkingRepository) + executorService ); //when diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/fake/CircuitBreakerTestService.java b/app-scheduler/src/test/java/com/parkingcomestrue/fake/CircuitBreakerTestService.java new file mode 100644 index 00000000..7137e307 --- /dev/null +++ b/app-scheduler/src/test/java/com/parkingcomestrue/fake/CircuitBreakerTestService.java @@ -0,0 +1,15 @@ +package com.parkingcomestrue.fake; + +import com.parkingcomestrue.external.api.CircuitBreaker; +import java.util.concurrent.TimeUnit; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Component +public class CircuitBreakerTestService { + + @CircuitBreaker(resetTime = 2, timeUnit = TimeUnit.SECONDS) + public void call(Runnable runnable) { + runnable.run(); + } +} diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/fake/ExceptionParkingApiService.java b/app-scheduler/src/test/java/com/parkingcomestrue/fake/ExceptionParkingApiService.java index 0a801903..97da6efb 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/fake/ExceptionParkingApiService.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/fake/ExceptionParkingApiService.java @@ -1,8 +1,9 @@ package com.parkingcomestrue.fake; +import com.parkingcomestrue.external.api.HealthCheckResponse; import com.parkingcomestrue.external.support.exception.SchedulerException; import com.parkingcomestrue.external.support.exception.SchedulerExceptionInformation; -import com.parkingcomestrue.external.parkingapi.ParkingApiService; +import com.parkingcomestrue.external.api.parkingapi.ParkingApiService; import com.parkingcomestrue.common.domain.parking.Parking; import java.util.List; @@ -14,7 +15,17 @@ public boolean offerCurrentParking() { } @Override - public List read() { + public List read(int pageNumber, int size) { throw new SchedulerException(SchedulerExceptionInformation.INVALID_CONNECT); } + + @Override + public int getReadSize() { + return 0; + } + + @Override + public HealthCheckResponse check() { + return new HealthCheckResponse(false, 0); + } } diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/fake/FakeCoordinateApiService.java b/app-scheduler/src/test/java/com/parkingcomestrue/fake/FakeCoordinateApiService.java index 567ecf57..47440730 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/fake/FakeCoordinateApiService.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/fake/FakeCoordinateApiService.java @@ -1,6 +1,6 @@ package com.parkingcomestrue.fake; -import com.parkingcomestrue.external.coordinate.CoordinateApiService; +import com.parkingcomestrue.external.api.coordinate.CoordinateApiService; import com.parkingcomestrue.common.domain.parking.Location; public class FakeCoordinateApiService extends CoordinateApiService { diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/fake/FakeParkingBatchRepository.java b/app-scheduler/src/test/java/com/parkingcomestrue/fake/FakeParkingBatchRepository.java new file mode 100644 index 00000000..35bce493 --- /dev/null +++ b/app-scheduler/src/test/java/com/parkingcomestrue/fake/FakeParkingBatchRepository.java @@ -0,0 +1,48 @@ +package com.parkingcomestrue.fake; + +import com.parkingcomestrue.external.respository.ParkingBatchRepository; +import com.parkingcomestrue.common.domain.parking.Parking; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.locationtech.jts.geom.Point; +import repository.BasicParkingRepository; + +public class FakeParkingBatchRepository implements ParkingBatchRepository { + + BasicParkingRepository parkingRepository = new BasicParkingRepository(); + + @Override + public void saveWithBatch(List parkingLots) { + parkingRepository.saveAll(parkingLots); + } + + @Override + public Optional findById(Long id) { + return parkingRepository.findById(id); + } + + @Override + public List findAroundParkingLots(Point point, int radius) { + return parkingRepository.findAroundParkingLots(point, radius); + } + + @Override + public List findAroundParkingLotsOrderByDistance(Point point, int radius) { + return parkingRepository.findAroundParkingLotsOrderByDistance(point, radius); + } + + @Override + public Set findAllByBaseInformationNameIn(Set parkingNames) { + return parkingRepository.findAllByBaseInformationNameIn(parkingNames); + } + + @Override + public void saveAll(Iterable parkingLots) { + parkingRepository.saveAll(parkingLots); + } + + public int count() { + return parkingRepository.count(); + } +} diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/fake/NotOfferCurrentParkingApiService.java b/app-scheduler/src/test/java/com/parkingcomestrue/fake/NotOfferCurrentParkingApiService.java index 20cbbf48..d2907036 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/fake/NotOfferCurrentParkingApiService.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/fake/NotOfferCurrentParkingApiService.java @@ -12,7 +12,8 @@ import com.parkingcomestrue.common.domain.parking.PayType; import com.parkingcomestrue.common.domain.parking.Space; import com.parkingcomestrue.common.domain.parking.TimeUnit; -import com.parkingcomestrue.external.parkingapi.ParkingApiService; +import com.parkingcomestrue.external.api.HealthCheckResponse; +import com.parkingcomestrue.external.api.parkingapi.ParkingApiService; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -31,9 +32,9 @@ public boolean offerCurrentParking() { } @Override - public List read() { + public List read(int pageNumber, int size) { LinkedList result = new LinkedList<>(); - for (int i = 0; i < readSize; i++) { + for (int i = 0; i < size; i++) { Parking parking = new Parking( new BaseInformation("not offer parking" + i, "051-000" + i, "부산시 어딘가 " + i, Set.of(PayType.NO_INFO), @@ -51,7 +52,17 @@ public List read() { return result; } + @Override + public int getReadSize() { + return readSize; + } + public void setReadSize(int readSize) { this.readSize = readSize; } + + @Override + public HealthCheckResponse check() { + return new HealthCheckResponse(true, readSize); + } } diff --git a/app-scheduler/src/test/java/com/parkingcomestrue/fake/OfferCurrentParkingApiService.java b/app-scheduler/src/test/java/com/parkingcomestrue/fake/OfferCurrentParkingApiService.java index 364d55a1..045c679e 100644 --- a/app-scheduler/src/test/java/com/parkingcomestrue/fake/OfferCurrentParkingApiService.java +++ b/app-scheduler/src/test/java/com/parkingcomestrue/fake/OfferCurrentParkingApiService.java @@ -12,7 +12,8 @@ import com.parkingcomestrue.common.domain.parking.PayType; import com.parkingcomestrue.common.domain.parking.Space; import com.parkingcomestrue.common.domain.parking.TimeUnit; -import com.parkingcomestrue.external.parkingapi.ParkingApiService; +import com.parkingcomestrue.external.api.HealthCheckResponse; +import com.parkingcomestrue.external.api.parkingapi.ParkingApiService; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -31,9 +32,9 @@ public boolean offerCurrentParking() { } @Override - public List read() { + public List read(int pageNumber, int size) { LinkedList result = new LinkedList<>(); - for (int i = 0; i < readSize; i++) { + for (int i = 0; i < size; i++) { Parking parking = new Parking( new BaseInformation("offer parking" + i, "02-000" + i, "서울시 어딘가 " + i, Set.of(PayType.NO_INFO), ParkingType.NO_INFO, @@ -50,7 +51,17 @@ public List read() { return result; } + @Override + public int getReadSize() { + return readSize; + } + public void setReadSize(int readSize) { this.readSize = readSize; } + + @Override + public HealthCheckResponse check() { + return new HealthCheckResponse(true, readSize); + } } diff --git a/app-scheduler/src/test/resources/application.yml b/app-scheduler/src/test/resources/application.yml new file mode 100644 index 00000000..1e8049ae --- /dev/null +++ b/app-scheduler/src/test/resources/application.yml @@ -0,0 +1,13 @@ +spring: + profiles: + active: ${PROFILE:dev} + + flyway: + enabled: false + +# API KEY +kakao: + key: ${KAKAO_API_KEY:kakao} +seoul-public-parking-key: ${SEOUL_API_KEY:seoul} +pusan-public-parking-key: ${PUSAN_API_KEY:pusan} +korea-parking-key: ${KOREA_API_KEY:korea} diff --git a/domain/src/main/java/com/parkingcomestrue/common/domain/AuditingEntity.java b/domain/src/main/java/com/parkingcomestrue/common/domain/AuditingEntity.java index fb3139fe..74950574 100644 --- a/domain/src/main/java/com/parkingcomestrue/common/domain/AuditingEntity.java +++ b/domain/src/main/java/com/parkingcomestrue/common/domain/AuditingEntity.java @@ -1,5 +1,6 @@ package com.parkingcomestrue.common.domain; +import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; @@ -14,8 +15,9 @@ public abstract class AuditingEntity { @CreatedDate - private LocalDateTime createdAt; + @Column(updatable = false) + protected LocalDateTime createdAt; @LastModifiedDate - private LocalDateTime updatedAt; + protected LocalDateTime updatedAt; } diff --git a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTime.java b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTime.java index 46d742dd..9b2447a7 100644 --- a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTime.java +++ b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTime.java @@ -1,15 +1,18 @@ package com.parkingcomestrue.common.domain.parking; -import jakarta.persistence.AttributeOverride; -import jakarta.persistence.Column; +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; + +import com.parkingcomestrue.common.infra.converter.TimeInfoConverter; +import jakarta.persistence.Convert; import jakarta.persistence.Embeddable; -import jakarta.persistence.Embedded; import java.time.LocalTime; import lombok.AccessLevel; +import lombok.Getter; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Embeddable +@Getter public class FreeOperatingTime { public static final FreeOperatingTime ALWAYS_PAY = new FreeOperatingTime(TimeInfo.CLOSED, TimeInfo.CLOSED, @@ -17,25 +20,19 @@ public class FreeOperatingTime { public static final FreeOperatingTime ALWAYS_FREE = new FreeOperatingTime(TimeInfo.ALL_DAY, TimeInfo.ALL_DAY, TimeInfo.ALL_DAY); - @AttributeOverride(name = "beginTime", column = @Column(name = "weekday_free_begin_time")) - @AttributeOverride(name = "endTime", column = @Column(name = "weekday_free_end_time")) - @Embedded - private TimeInfo weekday; + @Convert(converter = TimeInfoConverter.class) + private TimeInfo weekdayFreeOperatingTime; - @AttributeOverride(name = "beginTime", column = @Column(name = "saturday_free_begin_time")) - @AttributeOverride(name = "endTime", column = @Column(name = "saturday_free_end_time")) - @Embedded - private TimeInfo saturday; + @Convert(converter = TimeInfoConverter.class) + private TimeInfo saturdayFreeOperatingTime; - @AttributeOverride(name = "beginTime", column = @Column(name = "holiday_free_begin_time")) - @AttributeOverride(name = "endTime", column = @Column(name = "holiday_free_end_time")) - @Embedded - private TimeInfo holiday; + @Convert(converter = TimeInfoConverter.class) + private TimeInfo holidayFreeOperatingTime; - public FreeOperatingTime(TimeInfo weekday, TimeInfo saturday, TimeInfo holiday) { - this.weekday = weekday; - this.saturday = saturday; - this.holiday = holiday; + public FreeOperatingTime(TimeInfo weekdayFreeOperatingTime, TimeInfo saturdayFreeOperatingTime, TimeInfo holidayFreeOperatingTime) { + this.weekdayFreeOperatingTime = weekdayFreeOperatingTime; + this.saturdayFreeOperatingTime = saturdayFreeOperatingTime; + this.holidayFreeOperatingTime = holidayFreeOperatingTime; } public int calculateNonFreeUsageMinutes(DayParking dayParking) { @@ -52,12 +49,12 @@ public int calculateNonFreeUsageMinutes(DayParking dayParking) { private TimeInfo getTodayTimeInfo(DayParking dayParking) { if (dayParking.isWeekDay()) { - return weekday; + return weekdayFreeOperatingTime; } if (dayParking.isSaturday()) { - return saturday; + return saturdayFreeOperatingTime; } - return holiday; + return holidayFreeOperatingTime; } private boolean isFreeDay(TimeInfo today) { @@ -65,7 +62,7 @@ private boolean isFreeDay(TimeInfo today) { } private int calculateMinutes(LocalTime localTime) { - if (localTime.equals(LocalTime.MAX)) { + if (localTime.equals(MAX_END_TIME)) { return localTime.getHour() * 60 + localTime.getMinute() + 1; } return localTime.getHour() * 60 + localTime.getMinute(); diff --git a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Location.java b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Location.java index a1b86726..daa7e2e0 100644 --- a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Location.java +++ b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Location.java @@ -39,7 +39,7 @@ public static Location of(Double longitude, Double latitude) { try { verifyLocation(longitude, latitude); return new Location(longitude, latitude); - } catch (NullPointerException e) { + } catch (NullPointerException | DomainException e) { return NO_PROVIDE; } } diff --git a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/OperatingTime.java b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/OperatingTime.java index 65cdde82..5d06f9d5 100644 --- a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/OperatingTime.java +++ b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/OperatingTime.java @@ -1,9 +1,8 @@ package com.parkingcomestrue.common.domain.parking; -import jakarta.persistence.AttributeOverride; -import jakarta.persistence.Column; +import com.parkingcomestrue.common.infra.converter.TimeInfoConverter; +import jakarta.persistence.Convert; import jakarta.persistence.Embeddable; -import jakarta.persistence.Embedded; import java.time.LocalTime; import lombok.AccessLevel; import lombok.Getter; @@ -17,50 +16,44 @@ public class OperatingTime { public static final OperatingTime ALWAYS_OPEN = new OperatingTime(TimeInfo.ALL_DAY, TimeInfo.ALL_DAY, TimeInfo.ALL_DAY); - @AttributeOverride(name = "beginTime", column = @Column(name = "weekday_begin_time")) - @AttributeOverride(name = "endTime", column = @Column(name = "weekday_end_time")) - @Embedded - private TimeInfo weekday; + @Convert(converter = TimeInfoConverter.class) + private TimeInfo weekdayOperatingTime; - @AttributeOverride(name = "beginTime", column = @Column(name = "saturday_begin_time")) - @AttributeOverride(name = "endTime", column = @Column(name = "saturday_end_time")) - @Embedded - private TimeInfo saturday; + @Convert(converter = TimeInfoConverter.class) + private TimeInfo saturdayOperatingTime; - @AttributeOverride(name = "beginTime", column = @Column(name = "holiday_begin_time")) - @AttributeOverride(name = "endTime", column = @Column(name = "holiday_end_time")) - @Embedded - private TimeInfo holiday; + @Convert(converter = TimeInfoConverter.class) + private TimeInfo holidayOperatingTime; - public OperatingTime(TimeInfo weekday, - TimeInfo saturday, - TimeInfo holiday) { - this.weekday = weekday; - this.saturday = saturday; - this.holiday = holiday; + public OperatingTime(TimeInfo weekdayOperatingTime, + TimeInfo saturdayOperatingTime, + TimeInfo holidayOperatingTime) { + this.weekdayOperatingTime = weekdayOperatingTime; + this.saturdayOperatingTime = saturdayOperatingTime; + this.holidayOperatingTime = holidayOperatingTime; } public LocalTime getWeekdayBeginTime() { - return weekday.getBeginTime(); + return weekdayOperatingTime.getBeginTime(); } public LocalTime getWeekdayEndTime() { - return weekday.getEndTime(); + return weekdayOperatingTime.getEndTime(); } public LocalTime getSaturdayBeginTime() { - return saturday.getBeginTime(); + return saturdayOperatingTime.getBeginTime(); } public LocalTime getSaturdayEndTime() { - return saturday.getEndTime(); + return saturdayOperatingTime.getEndTime(); } public LocalTime getHolidayBeginTime() { - return holiday.getBeginTime(); + return holidayOperatingTime.getBeginTime(); } public LocalTime getHolidayEndTime() { - return holiday.getEndTime(); + return holidayOperatingTime.getEndTime(); } } diff --git a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Parking.java b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Parking.java index 239b85cb..dbd29af0 100644 --- a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Parking.java +++ b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/Parking.java @@ -58,6 +58,7 @@ private Parking(Long id, BaseInformation baseInformation, Location location, Spa this.freeOperatingTime = freeOperatingTime; this.operatingTime = operatingTime; this.feePolicy = feePolicy; + this.createdAt = LocalDateTime.now(); } public Parking(BaseInformation baseInformation, Location location, Space space, @@ -68,6 +69,8 @@ public Parking(BaseInformation baseInformation, Location location, Space space, this.freeOperatingTime = freeOperatingTime; this.operatingTime = operatingTime; this.feePolicy = feePolicy; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); } public int calculatePayOfChargeMinutes(DayParking dayParking) { @@ -87,10 +90,12 @@ public void update(Parking updated) { this.freeOperatingTime = updated.freeOperatingTime; this.operatingTime = updated.operatingTime; this.feePolicy = updated.feePolicy; + this.updatedAt = LocalDateTime.now(); } public void update(Location location) { this.location = location; + this.updatedAt = LocalDateTime.now(); } public boolean containsOperationType(Set operationTypes) { @@ -138,6 +143,10 @@ public int calculateUpdatedDiff(LocalDateTime now) { return diffMinute.intValue(); } + public boolean isLocationAvailable() { + return !location.equals(Location.NO_PROVIDE); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculator.java b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculator.java index fbf8c8d2..f61f424c 100644 --- a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculator.java +++ b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculator.java @@ -1,5 +1,7 @@ package com.parkingcomestrue.common.domain.parking; +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; + import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; @@ -36,7 +38,7 @@ private List separateDate(LocalDateTime beginTime, LocalDateTime end dayParkingDates.add(makeFirstDayParking(beginTime)); beginTime = beginTime.plusDays(1); while (!isSameDate(beginTime, endTime)) { - dayParkingDates.add(new DayParking(Day.from(beginTime.getDayOfWeek()), LocalTime.MIN, LocalTime.MAX)); + dayParkingDates.add(new DayParking(Day.from(beginTime.getDayOfWeek()), LocalTime.MIN, MAX_END_TIME)); beginTime = beginTime.plusDays(1); } dayParkingDates.add(makeLastDayParking(endTime)); @@ -62,7 +64,7 @@ private boolean isSameDayOfMonth(LocalDateTime beginTime, LocalDateTime endTime) } private DayParking makeFirstDayParking(LocalDateTime beginTime) { - return new DayParking(Day.from(beginTime.getDayOfWeek()), beginTime.toLocalTime(), LocalTime.MAX); + return new DayParking(Day.from(beginTime.getDayOfWeek()), beginTime.toLocalTime(), MAX_END_TIME); } private DayParking makeLastDayParking(LocalDateTime endTime) { diff --git a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/TimeInfo.java b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/TimeInfo.java index 5d209a83..be58f636 100644 --- a/domain/src/main/java/com/parkingcomestrue/common/domain/parking/TimeInfo.java +++ b/domain/src/main/java/com/parkingcomestrue/common/domain/parking/TimeInfo.java @@ -2,6 +2,7 @@ import jakarta.persistence.Embeddable; import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -14,7 +15,10 @@ public class TimeInfo { public static final TimeInfo CLOSED = new TimeInfo(LocalTime.MIN, LocalTime.MIN); - public static final TimeInfo ALL_DAY = new TimeInfo(LocalTime.MIN, LocalTime.MAX); + public static final LocalTime MAX_END_TIME = LocalTime.of(23, 59); + public static final TimeInfo ALL_DAY = new TimeInfo(LocalTime.MIN, MAX_END_TIME); + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + private static final String DELIMITER = "~"; private LocalTime beginTime; private LocalTime endTime; @@ -26,7 +30,7 @@ public TimeInfo(LocalTime beginTime, LocalTime endTime) { public int calculateOverlapMinutes(LocalTime beginTime, LocalTime endTime) { if (this.endTime.isBefore(this.beginTime)) { - TimeInfo today = new TimeInfo(this.beginTime, LocalTime.MAX); + TimeInfo today = new TimeInfo(this.beginTime, MAX_END_TIME); TimeInfo tomorrow = new TimeInfo(LocalTime.MIN, this.endTime); return today.calculateOverlapMinutes(beginTime, endTime) + tomorrow.calculateOverlapMinutes(beginTime, endTime); @@ -56,9 +60,16 @@ private int calculateBetweenMinutes(LocalTime beginTime, LocalTime endTime) { } private int calculateMinutes(LocalTime localTime) { - if (localTime.equals(LocalTime.MAX)) { + if (localTime.equals(MAX_END_TIME)) { return localTime.getHour() * 60 + localTime.getMinute() + 1; } return localTime.getHour() * 60 + localTime.getMinute(); } + + @Override + public String toString() { + String beginTime = this.beginTime.format(TIME_FORMATTER); + String endTime = this.endTime.format(TIME_FORMATTER); + return beginTime + DELIMITER + endTime; + } } diff --git a/domain/src/main/java/com/parkingcomestrue/common/infra/converter/TimeInfoConverter.java b/domain/src/main/java/com/parkingcomestrue/common/infra/converter/TimeInfoConverter.java new file mode 100644 index 00000000..4a996083 --- /dev/null +++ b/domain/src/main/java/com/parkingcomestrue/common/infra/converter/TimeInfoConverter.java @@ -0,0 +1,25 @@ +package com.parkingcomestrue.common.infra.converter; + +import com.parkingcomestrue.common.domain.parking.TimeInfo; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +@Converter +public class TimeInfoConverter implements AttributeConverter { + + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + private static final String DELIMITER = "~"; + + @Override + public String convertToDatabaseColumn(TimeInfo attribute) { + return attribute.toString(); + } + + @Override + public TimeInfo convertToEntityAttribute(String dbData) { + String[] times = dbData.split(DELIMITER); + return new TimeInfo(LocalTime.parse(times[0], TIME_FORMATTER), LocalTime.parse(times[1], TIME_FORMATTER)); + } +} diff --git a/domain/src/main/resources/db/migration/mysql/V5.0.0__change_column_name.sql b/domain/src/main/resources/db/migration/mysql/V5.0.0__change_column_name.sql new file mode 100644 index 00000000..464912cd --- /dev/null +++ b/domain/src/main/resources/db/migration/mysql/V5.0.0__change_column_name.sql @@ -0,0 +1,24 @@ +ALTER TABLE parking + DROP COLUMN holiday_begin_time, + DROP COLUMN holiday_end_time, + DROP COLUMN holiday_free_begin_time, + DROP COLUMN holiday_free_end_time, + DROP COLUMN saturday_begin_time, + DROP COLUMN saturday_end_time, + DROP COLUMN saturday_free_begin_time, + DROP COLUMN saturday_free_end_time, + DROP COLUMN weekday_begin_time, + DROP COLUMN weekday_end_time, + DROP COLUMN weekday_free_begin_time, + DROP COLUMN weekday_free_end_time; + +ALTER TABLE parking + ADD COLUMN holiday_operating_time VARCHAR(20), + ADD COLUMN holiday_free_operating_time VARCHAR(20), + ADD COLUMN saturday_operating_time VARCHAR(20), + ADD COLUMN saturday_free_operating_time VARCHAR(20), + ADD COLUMN weekday_operating_time VARCHAR(20), + ADD COLUMN weekday_free_operating_time VARCHAR(20); + + + diff --git a/domain/src/test/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTimeTest.java b/domain/src/test/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTimeTest.java index 6d34ed68..5c5e40ec 100644 --- a/domain/src/test/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTimeTest.java +++ b/domain/src/test/java/com/parkingcomestrue/common/domain/parking/FreeOperatingTimeTest.java @@ -1,5 +1,7 @@ package com.parkingcomestrue.common.domain.parking; +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; + import com.parkingcomestrue.common.domain.parking.Day; import com.parkingcomestrue.common.domain.parking.DayParking; import com.parkingcomestrue.common.domain.parking.FreeOperatingTime; @@ -36,7 +38,7 @@ static Stream getFreeOperatingTime() { */ Arguments.of( FreeOperatingTime.ALWAYS_FREE, - new DayParking(Day.WEEKDAY, LocalTime.MIN, LocalTime.MAX), + new DayParking(Day.WEEKDAY, LocalTime.MIN, MAX_END_TIME), 0 ), /* @@ -58,7 +60,7 @@ static Stream getFreeOperatingTime() { TimeInfo.ALL_DAY, TimeInfo.ALL_DAY ), - new DayParking(Day.WEEKDAY, LocalTime.MIN, LocalTime.MAX), + new DayParking(Day.WEEKDAY, LocalTime.MIN, MAX_END_TIME), 1200 ), /* diff --git a/domain/src/test/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculatorTest.java b/domain/src/test/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculatorTest.java index d0cd9544..79e96b33 100644 --- a/domain/src/test/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculatorTest.java +++ b/domain/src/test/java/com/parkingcomestrue/common/domain/parking/ParkingFeeCalculatorTest.java @@ -1,5 +1,7 @@ package com.parkingcomestrue.common.domain.parking; +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; + import com.parkingcomestrue.common.domain.parking.BaseInformation; import com.parkingcomestrue.common.domain.parking.Fee; import com.parkingcomestrue.common.domain.parking.FeePolicy; @@ -267,7 +269,7 @@ static Stream getParkingFeeCalculator() { ), new TimeInfo( LocalTime.of(6, 0), - LocalTime.MAX + MAX_END_TIME ), TimeInfo.ALL_DAY ), @@ -325,7 +327,7 @@ static Stream getParkingFeeCalculator() { TimeInfo.ALL_DAY, new TimeInfo( LocalTime.of(4, 0), - LocalTime.MAX + MAX_END_TIME ) ), new OperatingTime(), @@ -384,7 +386,7 @@ static Stream getParkingFeeCalculator() { TimeInfo.ALL_DAY, new TimeInfo( LocalTime.of(4, 0), - LocalTime.MAX + MAX_END_TIME ) ), new OperatingTime(), diff --git a/domain/src/test/java/com/parkingcomestrue/common/domain/parking/TimeInfoTest.java b/domain/src/test/java/com/parkingcomestrue/common/domain/parking/TimeInfoTest.java index ffaefbe0..2814a63c 100644 --- a/domain/src/test/java/com/parkingcomestrue/common/domain/parking/TimeInfoTest.java +++ b/domain/src/test/java/com/parkingcomestrue/common/domain/parking/TimeInfoTest.java @@ -1,6 +1,7 @@ package com.parkingcomestrue.common.domain.parking; -import com.parkingcomestrue.common.domain.parking.TimeInfo; +import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME; + import java.time.LocalTime; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -26,9 +27,9 @@ static Stream getTimeInfos() { 겹침 시간 : 00:00 ~ 24:00 -> 1440분 */ Arguments.of( - new TimeInfo(LocalTime.MIN, LocalTime.MAX), + new TimeInfo(LocalTime.MIN, MAX_END_TIME), LocalTime.MIN, - LocalTime.MAX, + MAX_END_TIME, 1440), /* 무료 운영 시간 : 00:00 ~ 24:00 @@ -36,7 +37,7 @@ static Stream getTimeInfos() { 겹침 시간 : 01:30 ~ 22:11 -> 1241분 */ Arguments.of( - new TimeInfo(LocalTime.MIN, LocalTime.MAX), + new TimeInfo(LocalTime.MIN, MAX_END_TIME), LocalTime.of(1, 30), LocalTime.of(22, 11), 1241), @@ -118,7 +119,7 @@ static Stream getTimeInfos() { Arguments.of( new TimeInfo(LocalTime.of(21, 0), LocalTime.of(9, 0)), LocalTime.of(21, 0), - LocalTime.MAX, + MAX_END_TIME, 180), /* 무료 운영 시간 : 21:00 ~ 09:00 diff --git a/domain/src/test/java/com/parkingcomestrue/common/infra/converter/TimeInfoConverterTest.java b/domain/src/test/java/com/parkingcomestrue/common/infra/converter/TimeInfoConverterTest.java new file mode 100644 index 00000000..6ec22b72 --- /dev/null +++ b/domain/src/test/java/com/parkingcomestrue/common/infra/converter/TimeInfoConverterTest.java @@ -0,0 +1,36 @@ +package com.parkingcomestrue.common.infra.converter; + +import com.parkingcomestrue.common.domain.parking.TimeInfo; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class TimeInfoConverterTest { + + private TimeInfoConverter converter = new TimeInfoConverter(); + + @Test + void TimeInfo에서_컬럼으로_변경() { + //given + TimeInfo allDay = TimeInfo.ALL_DAY; + String expected = "00:00~23:59"; + + //when + String actual = converter.convertToDatabaseColumn(allDay); + + //then + Assertions.assertThat(actual).isEqualTo(expected); + } + + @Test + void 컬럼에서_TimeInfo로_변경() { + //given + String column = "00:00~23:59"; + TimeInfo expected = TimeInfo.ALL_DAY; + + //when + TimeInfo actual = converter.convertToEntityAttribute(column); + + //then + Assertions.assertThat(actual).isEqualTo(expected); + } +}