diff --git a/data/mysql-files/bus_route_station.csv b/data/mysql-files/bus_route_station.csv index f3c2fc00..f5d039dc 100644 --- a/data/mysql-files/bus_route_station.csv +++ b/data/mysql-files/bus_route_station.csv @@ -152020,3 +152020,4 @@ routeId,stationId,stationSeq,stationName,mobileNo,regionName,districtCd,centerYn 249000002,206000564,5,봇들육교,07510,성남,2,N,N,127.1076667,37.4033667 249000002,206000540,6,삼평교,07496,성남,2,N,N,127.1047833,37.4041333 249000002,204000341,7,기업성장센터,05332,성남,2,N,N,127.0948,37.4112833 +200000108,200000068,65,율전중학교,01002,수원,2,N,N,126.9658333,37.3010333 \ No newline at end of file diff --git a/data/mysql-files/bus_station.csv b/data/mysql-files/bus_station.csv index 4bb4f498..18091c1f 100644 --- a/data/mysql-files/bus_station.csv +++ b/data/mysql-files/bus_station.csv @@ -31187,3 +31187,4 @@ stationId,stationName,mobileNo,regionName,districtCd,centerYn,turnYn,x,y 234000329,풍산목재,38164,광주,2,N,N,127.2269,37.3608 234000328,오포1동행정복지센터.오포파출소,38534,광주,2,N,N,127.2318,37.3669 219000149,중산마을10단지.동신아파트,20259,고양,2,N,N,126.7788333,37.68725 +200000068,율전중학교,01002,수원,2,N,N,126.9658333,37.3010333 \ No newline at end of file diff --git a/server/build.gradle b/server/build.gradle index 0f62c3b8..f0cdbb29 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -33,8 +33,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // XML Parser implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + + // Spring Retry + implementation 'org.springframework.retry:spring-retry' + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' //Swagger + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationBodyDto.java b/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationBodyDto.java index a9f26d4c..3fdd897f 100644 --- a/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationBodyDto.java +++ b/server/src/main/java/com/talkka/server/api/datagg/dto/BusLocationBodyDto.java @@ -21,12 +21,12 @@ public BusLocationEntity toEntity(int apiCallNo, LocalDateTime createdAt) { return BusLocationEntity.builder() .apiRouteId(routeId) .apiStationId(stationId) - .stationSeq(stationSeq.shortValue()) + .stationSeq(stationSeq) .endBus(endBusEnum) .lowPlate(lowPlateEnum) .plateNo(plateNo) .plateType(plateTypeEnum) - .remainSeatCount(remainSeatCnt.shortValue()) + .remainSeatCount(remainSeatCnt) .apiCallNo(apiCallNo) .createdAt(createdAt) .build(); diff --git a/server/src/main/java/com/talkka/server/api/datagg/service/SimpleBusApiService.java b/server/src/main/java/com/talkka/server/api/datagg/service/SimpleBusApiService.java index 748361cd..43aacea5 100644 --- a/server/src/main/java/com/talkka/server/api/datagg/service/SimpleBusApiService.java +++ b/server/src/main/java/com/talkka/server/api/datagg/service/SimpleBusApiService.java @@ -7,9 +7,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; +import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; @@ -42,15 +46,10 @@ public List getSearchedRouteInfo(String keyword) throws A MultiValueMap params = new LinkedMultiValueMap<>(); params.add("keyword", keyword); try { - URI uri = this.getOpenApiUri(path, params); - ResponseEntity resp = restTemplate.getForEntity(uri, BusRouteSearchRespDto.class); - var body = resp.getBody().msgBody(); - if (body == null) { - throw new ApiClientException("결과가 없습니다."); - } - return body; - } catch (Exception exception) { - throw new ApiClientException(exception.getMessage()); + ResponseEntity resp = apiCallWithRetry(path, params, BusRouteSearchRespDto.class); + return resp.getBody().msgBody(); + } catch (RestClientException exception) { + throw new ApiClientException("결과가 없습니다."); } } @@ -60,15 +59,10 @@ public List getRouteInfo(String apiRouteId) throws ApiClien MultiValueMap params = new LinkedMultiValueMap<>(); params.add("routeId", apiRouteId); try { - URI uri = this.getOpenApiUri(path, params); - ResponseEntity resp = restTemplate.getForEntity(uri, BusRouteInfoRespDto.class); - var body = resp.getBody().msgBody(); - if (body == null) { - throw new ApiClientException("결과가 없습니다."); - } - return body; - } catch (Exception exception) { - throw new ApiClientException(exception.getMessage()); + ResponseEntity resp = apiCallWithRetry(path, params, BusRouteInfoRespDto.class); + return resp.getBody().msgBody(); + } catch (RestClientException exception) { + throw new ApiClientException("결과가 없습니다."); } } @@ -78,15 +72,10 @@ public List getRouteStationInfo(String apiRouteId) throw MultiValueMap params = new LinkedMultiValueMap<>(); params.add("routeId", apiRouteId); try { - URI uri = this.getOpenApiUri(path, params); - ResponseEntity resp = restTemplate.getForEntity(uri, BusRouteStationRespDto.class); - var body = resp.getBody().msgBody(); - if (body == null) { - throw new ApiClientException("결과가 없습니다."); - } - return body; - } catch (Exception exception) { - throw new ApiClientException(exception.getMessage()); + ResponseEntity resp = apiCallWithRetry(path, params, BusRouteStationRespDto.class); + return resp.getBody().msgBody(); + } catch (RestClientException exception) { + throw new ApiClientException("결과가 없습니다."); } } @@ -96,15 +85,10 @@ public List getBusLocationInfo(String apiRouteId) throws Api MultiValueMap params = new LinkedMultiValueMap<>(); params.add("routeId", apiRouteId); try { - URI uri = this.getOpenApiUri(path, params); - ResponseEntity resp = restTemplate.getForEntity(uri, BusLocationRespDto.class); - var body = resp.getBody().msgBody(); - if (body == null) { - throw new ApiClientException("결과가 없습니다."); - } - return body; - } catch (Exception exception) { - throw new ApiClientException(exception.getMessage()); + ResponseEntity resp = apiCallWithRetry(path, params, BusLocationRespDto.class); + return resp.getBody().msgBody(); + } catch (RestClientException exception) { + throw new ApiClientException("결과가 없습니다."); } } @@ -139,4 +123,31 @@ private URI getOpenApiUri(String path, MultiValueMap params) { .queryParams(params) .build(); } + + // 리트라이 로직을 포함한 api call + private ResponseEntity apiCallWithRetry(String path, MultiValueMap params, + Class type) throws RestClientException { + + final int MAX_ATTEMPTS = 10; + final int RETRY_INTERVAL = 200; + + // 이후 bean 으로 등록하는것 고려 + RetryTemplate retryTemplate = new RetryTemplate(); + + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); + retryPolicy.setMaxAttempts(MAX_ATTEMPTS); + retryTemplate.setRetryPolicy(retryPolicy); + + FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); + backOffPolicy.setBackOffPeriod(RETRY_INTERVAL); + retryTemplate.setBackOffPolicy(backOffPolicy); + + // 재시도마다 새로운 api key 로 시도 + // 파싱 실패시 RestClientException 터트림 + return retryTemplate.execute(context -> { + // 재시도마다 새로운 api key 로 시도 + URI uri = this.getOpenApiUri(path, params); + return restTemplate.getForEntity(uri, type); // 파싱 실패시 RestClientException 터트림 + }); + } } diff --git a/server/src/main/java/com/talkka/server/bus/controller/BusLocationProcessController.java b/server/src/main/java/com/talkka/server/bus/controller/BusLocationProcessController.java new file mode 100644 index 00000000..2b32bc6a --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/controller/BusLocationProcessController.java @@ -0,0 +1,24 @@ +package com.talkka.server.bus.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.talkka.server.bus.dao.BusLocationRepository; +import com.talkka.server.bus.service.BusLocationProcessor; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/bus/stat") +public class BusLocationProcessController { + private final BusLocationProcessor busLocationProcessor; + private final BusLocationRepository busLocationRepository; + + @PostMapping("/process/all") + public String process() { + busLocationProcessor.start(busLocationRepository.findAll()); + return "success"; + } +} diff --git a/server/src/main/java/com/talkka/server/bus/controller/BusViewController.java b/server/src/main/java/com/talkka/server/bus/controller/BusViewController.java new file mode 100644 index 00000000..b53e8ffb --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/controller/BusViewController.java @@ -0,0 +1,34 @@ +package com.talkka.server.bus.controller; + +import java.time.LocalDateTime; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.talkka.server.bus.dto.BusViewDto; +import com.talkka.server.bus.service.BusViewService; + +import lombok.RequiredArgsConstructor; + +// 테스트용 컨트롤러입니다. +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/bus/route-info") +public class BusViewController { + private final BusViewService busViewService; + + @GetMapping("/now") + public ResponseEntity getNow( + @RequestParam(name = "routeStationId", required = false, defaultValue = "17499") Long routeStationId, + @RequestParam(name = "stationNum", required = false, defaultValue = "5") Integer stationNum, + @RequestParam(name = "timeRange", required = false, defaultValue = "45") Integer timeRange, + @RequestParam(name = "week", required = false, defaultValue = "2") Long week + ) { + LocalDateTime time = LocalDateTime.now().plusHours(8); + return ResponseEntity.ok(busViewService.getBusView(routeStationId, stationNum, time, timeRange, week)); + } + +} diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusLocationEntity.java b/server/src/main/java/com/talkka/server/bus/dao/BusLocationEntity.java index 3a910d8c..1406003c 100644 --- a/server/src/main/java/com/talkka/server/bus/dao/BusLocationEntity.java +++ b/server/src/main/java/com/talkka/server/bus/dao/BusLocationEntity.java @@ -42,7 +42,7 @@ public class BusLocationEntity { private String apiStationId; @Column(name = "station_seq", nullable = false) - private Short stationSeq; + private Integer stationSeq; @Column(name = "end_bus", nullable = false, length = 1) @Convert(converter = EndBusConverter.class) @@ -60,7 +60,7 @@ public class BusLocationEntity { private PlateType plateType; @Column(name = "remain_seat_count", nullable = false) - private Short remainSeatCount; + private Integer remainSeatCount; @Column(name = "api_call_no", nullable = false) private Integer apiCallNo; diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusLocationRepository.java b/server/src/main/java/com/talkka/server/bus/dao/BusLocationRepository.java index 41cfa83f..fd8d14aa 100644 --- a/server/src/main/java/com/talkka/server/bus/dao/BusLocationRepository.java +++ b/server/src/main/java/com/talkka/server/bus/dao/BusLocationRepository.java @@ -1,8 +1,23 @@ package com.talkka.server.bus.dao; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; - + @Repository public interface BusLocationRepository extends JpaRepository { -} + + @Query(value = "select distinct l.apiCallNo from bus_location l") + List getDistinctApiCallNoList(); + + @Query(value = "select count(*) from bus_location") + Integer getRowNum(); + + List findByApiCallNo(Integer apiCallNo); + + List findByCreatedAtBetween(LocalDateTime startTime, LocalDateTime endTime); + +} \ No newline at end of file diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusPlateStatisticEntity.java b/server/src/main/java/com/talkka/server/bus/dao/BusPlateStatisticEntity.java new file mode 100644 index 00000000..e5af9bb7 --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/dao/BusPlateStatisticEntity.java @@ -0,0 +1,56 @@ +package com.talkka.server.bus.dao; + +import java.util.List; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import com.talkka.server.bus.enums.PlateType; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "bus_plate_statistic") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class BusPlateStatisticEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "route_id", nullable = false) + private BusRouteEntity route; + + @Column(name = "plate_type", nullable = false) + private PlateType plateType; + + @Column(name = "plate_no", nullable = false) + private String plateNo; + + @Column(name = "epoch_day", nullable = false) + private Long epochDay; + + @Column(name = "start_time", nullable = false) + private Integer startTime; + + @Column(name = "end_time", nullable = false) + private Integer endTime; + + @OneToMany(mappedBy = "plateStatistic", cascade = CascadeType.PERSIST) + private List seats; +} diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusPlateStatisticRepository.java b/server/src/main/java/com/talkka/server/bus/dao/BusPlateStatisticRepository.java new file mode 100644 index 00000000..a8407bcf --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/dao/BusPlateStatisticRepository.java @@ -0,0 +1,8 @@ +package com.talkka.server.bus.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BusPlateStatisticRepository extends JpaRepository { +} diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusRemainSeatEntity.java b/server/src/main/java/com/talkka/server/bus/dao/BusRemainSeatEntity.java new file mode 100644 index 00000000..35ed995c --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/dao/BusRemainSeatEntity.java @@ -0,0 +1,115 @@ +package com.talkka.server.bus.dao; + +import java.time.LocalDateTime; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import com.talkka.server.bus.enums.PlateType; +import com.talkka.server.bus.util.PlateTypeConverter; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "bus_remain_seat") +@Getter +@Builder(builderMethodName = "defaultBuilder") +@NoArgsConstructor +@AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class BusRemainSeatEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "route_id", nullable = false) + private BusRouteEntity route; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "station_id", nullable = false) + private BusStationEntity station; + + @Column(name = "station_seq", nullable = false) + private Integer stationSeq; + + @Column(name = "empty_seat", nullable = false) + private Integer emptySeat; + + @Column(name = "plate_no", nullable = false, length = 32) + private String plateNo; + + @Column(name = "plate_type", nullable = false, length = 1) + @Convert(converter = PlateTypeConverter.class) + private PlateType plateType; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "epoch_day", nullable = false) + private Long epochDay; + + @Column(name = "time", nullable = false) + private Integer time; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "plate_statistic_id") + private BusPlateStatisticEntity plateStatistic; + + // Builder Class + public static class BusRemainSeatEntityBuilder { + public BusRemainSeatEntityBuilder setCreateTime(LocalDateTime createdAt) { + this.createdAt = createdAt; + this.epochDay = getEpochDay(createdAt); + this.time = getTime(createdAt); + return this; + } + } + + public static BusRemainSeatEntityBuilder build() { + return defaultBuilder(); + } + + public void updateRouteInfo(BusPlateStatisticEntity plateStatistic) { + this.plateStatistic = plateStatistic; + } + + /** + * 주어진 {@code LocalDateTime} 객체의 시간과 분을 이어붙여 정수 형태로 반환합니다. + * 예를 들어, 23:59는 2359로, 08:27은 827로 반환됩니다. + * + * @param localDateTime 시간을 추출할 {@code LocalDateTime} 객체입니다. + * @return 시간과 분을 이어붙인 정수 값입니다. + */ + private static int getTime(LocalDateTime localDateTime) { + return localDateTime.getHour() * 100 + localDateTime.getMinute(); + } + + /** + * 주어진 {@code LocalDateTime} 객체에 대해 새벽 3시를 기준으로 날짜를 변경하여 epochDay 값을 반환합니다. + * 시간대가 3시 이전인 경우, 전날의 epochDay 를 반환하며, 그렇지 않은 경우 해당 날짜의 epochDay 를 반환합니다. + * 반환되는 값은 1에서 7 사이의 값을 가집니다. + * + * @param localDateTime 날짜를 계산할 {@code LocalDateTime} 객체입니다. + * @return 새벽 3시를 기준으로 계산된 epochDay 값입니다. + */ + private static Long getEpochDay(LocalDateTime localDateTime) { + if (localDateTime.getHour() < 3) { + return localDateTime.toLocalDate().toEpochDay() - 1; + } + return localDateTime.toLocalDate().toEpochDay(); + } + +} diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusRemainSeatRepository.java b/server/src/main/java/com/talkka/server/bus/dao/BusRemainSeatRepository.java new file mode 100644 index 00000000..9ac5ad87 --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/dao/BusRemainSeatRepository.java @@ -0,0 +1,14 @@ +package com.talkka.server.bus.dao; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BusRemainSeatRepository extends JpaRepository { + List findByRouteIdAndStationIdAndEpochDayAndTimeBetween(Long routeId, Long stationId, + Long epochDay, Integer startTime, Integer endTime); + + List findByPlateStatisticAndStationSeqBetweenOrderByStationSeq( + BusPlateStatisticEntity routeInfo, + Integer startSeq, Integer endSeq); +} diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusRouteEntity.java b/server/src/main/java/com/talkka/server/bus/dao/BusRouteEntity.java index dc7cf632..f90e1b99 100644 --- a/server/src/main/java/com/talkka/server/bus/dao/BusRouteEntity.java +++ b/server/src/main/java/com/talkka/server/bus/dao/BusRouteEntity.java @@ -112,6 +112,11 @@ public class BusRouteEntity { @LastModifiedDate private LocalDateTime updatedAt; + // 집계용 엔티티이므로 연관관계 설정 안함 + // @OneToMany(mappedBy = "route") + // @Builder.Default + // private List routeLocations = new ArrayList<>(); + @OneToMany(mappedBy = "route") @Builder.Default private List stations = new ArrayList<>(); diff --git a/server/src/main/java/com/talkka/server/bus/dao/BusRouteStationRepository.java b/server/src/main/java/com/talkka/server/bus/dao/BusRouteStationRepository.java index dd8bf57f..1cad50f1 100644 --- a/server/src/main/java/com/talkka/server/bus/dao/BusRouteStationRepository.java +++ b/server/src/main/java/com/talkka/server/bus/dao/BusRouteStationRepository.java @@ -12,4 +12,9 @@ public interface BusRouteStationRepository extends JpaRepository findAllByStationId(Long stationId); List findAllByRouteIdAndStationId(Long routeId, Long stationId); + + // 노선의 이전/다음 정거장 존재 유무를 확인하기 위함 + BusRouteStationEntity findByRouteAndStationSeq(BusRouteEntity route, Short stationSeq); + + int countAllByRoute(BusRouteEntity route); } diff --git a/server/src/main/java/com/talkka/server/bus/dto/BusRemainSeatDto.java b/server/src/main/java/com/talkka/server/bus/dto/BusRemainSeatDto.java new file mode 100644 index 00000000..cd563ea8 --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/dto/BusRemainSeatDto.java @@ -0,0 +1,40 @@ +package com.talkka.server.bus.dto; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import com.talkka.server.bus.dao.BusRemainSeatEntity; +import com.talkka.server.bus.enums.PlateType; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +public class BusRemainSeatDto { + @Getter + @AllArgsConstructor + private static class SeatInfo { + LocalDateTime arrivedTime; + Integer remainSeat; + } + + private PlateType plateType; + private String plateNo; + private LocalDateTime standardTime; + private final List remainSeatList = new ArrayList<>(); + + public BusRemainSeatDto(List seatList) { + for (var seat : seatList) { + plateType = seat.getPlateType(); + plateNo = seat.getPlateNo(); + remainSeatList.add(new SeatInfo(seat.getCreatedAt(), seat.getEmptySeat())); + } + if (!seatList.isEmpty()) { + // 기준 정거장을 기준으로 양 옆으로 조회하기 때문에 + // 기준 정거장은 배열의 정 가운데에 위치함 + int middleIdx = seatList.size() / 2; + standardTime = seatList.get(middleIdx).getCreatedAt(); + } + } +} diff --git a/server/src/main/java/com/talkka/server/bus/dto/BusViewDto.java b/server/src/main/java/com/talkka/server/bus/dto/BusViewDto.java new file mode 100644 index 00000000..e10793c6 --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/dto/BusViewDto.java @@ -0,0 +1,52 @@ +package com.talkka.server.bus.dto; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import com.talkka.server.bus.dao.BusRemainSeatEntity; +import com.talkka.server.bus.dao.BusRouteStationEntity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class BusViewDto { + + @Getter + @AllArgsConstructor + private static class StationInfo implements Serializable { + Long stationId; + String stationName; + } + + private LocalDateTime requestTime; + private Long routeId; + private String routeName; + private Integer stationNum; + private Integer busNum; + private List stationList; + private List data; + + public BusViewDto(LocalDateTime requestTime, List routeStationList, + List> data) { + this.requestTime = requestTime; + this.stationList = new ArrayList<>(); + for (var rs : routeStationList) { + stationList.add(new StationInfo(rs.getStation().getId(), rs.getStation().getStationName())); + this.routeId = rs.getRoute().getId(); + this.routeName = rs.getRoute().getRouteName(); + } + this.stationNum = routeStationList.size(); + this.busNum = data.size(); + this.data = data.stream() + .map(BusRemainSeatDto::new) + .sorted(Comparator.comparing(BusRemainSeatDto::getStandardTime)) + .toList(); + } +} diff --git a/server/src/main/java/com/talkka/server/bus/enums/EndBus.java b/server/src/main/java/com/talkka/server/bus/enums/EndBus.java index a365804b..2ed1e373 100644 --- a/server/src/main/java/com/talkka/server/bus/enums/EndBus.java +++ b/server/src/main/java/com/talkka/server/bus/enums/EndBus.java @@ -7,7 +7,7 @@ @Getter public enum EndBus implements EnumCodeInterface { // "0 = RUNNING" or "1 = END" - RUNNING("0"), END("1"), LENT_BUS_END("4"); + RUNNING("0"), END("1"), END_BUS_CODE2("2"), LENT_BUS_END("4"); private final String code; diff --git a/server/src/main/java/com/talkka/server/bus/exception/BusRouteNotFoundException.java b/server/src/main/java/com/talkka/server/bus/exception/BusRouteNotFoundException.java index 14d4097c..62b089e3 100644 --- a/server/src/main/java/com/talkka/server/bus/exception/BusRouteNotFoundException.java +++ b/server/src/main/java/com/talkka/server/bus/exception/BusRouteNotFoundException.java @@ -1,9 +1,9 @@ package com.talkka.server.bus.exception; public class BusRouteNotFoundException extends RuntimeException { - private static final String MESSAGE = "존재하지 않는 노선입니다. routeId: "; + private static final String MESSAGE = "존재하지 않는 노선입니다."; public BusRouteNotFoundException(Long routeId) { - super(MESSAGE + routeId); + super(MESSAGE + "routeId: " + routeId); } } diff --git a/server/src/main/java/com/talkka/server/bus/exception/InvalidLocationNotFoundException.java b/server/src/main/java/com/talkka/server/bus/exception/InvalidLocationNotFoundException.java new file mode 100644 index 00000000..f5cd0910 --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/exception/InvalidLocationNotFoundException.java @@ -0,0 +1,9 @@ +package com.talkka.server.bus.exception; + +public class InvalidLocationNotFoundException extends RuntimeException { + private static final String MESSAGE = "올바르지 않은 위치정보 입니다."; + + public InvalidLocationNotFoundException() { + super(MESSAGE); + } +} diff --git a/server/src/main/java/com/talkka/server/bus/service/BlockedApiLocationCollectService.java b/server/src/main/java/com/talkka/server/bus/service/BlockedApiLocationCollectService.java index 1e347125..a9ed170c 100644 --- a/server/src/main/java/com/talkka/server/bus/service/BlockedApiLocationCollectService.java +++ b/server/src/main/java/com/talkka/server/bus/service/BlockedApiLocationCollectService.java @@ -47,17 +47,16 @@ public void collectLocations() { log.info("No target routeId to collect bus locations"); return; } + apiRouteIdList.forEach(this::collectLocationsByRouteId); + } + @Override + @Transactional + public void collectLocationsByRouteId(String apiRouteId) throws ApiClientException { + log.info("Collecting bus locations for routeId: {}", apiRouteId); Integer apiCallNo = apiCallNumberProvider.getApiCallNumber(); LocalDateTime createdAt = LocalDateTime.now(); - for (String apiRouteId : apiRouteIdList) { - log.info("Collecting bus locations for routeId: {}", apiRouteId); - try { // should be refactored - List responseList = busApiService.getBusLocationInfo(apiRouteId); - busLocationService.saveBusLocations(responseList, apiCallNo, createdAt); - } catch (ApiClientException apiClientException) { - log.error("Failed to collect bus locations", apiClientException); - } - } + List responseList = busApiService.getBusLocationInfo(apiRouteId); + busLocationService.saveBusLocations(responseList, apiCallNo, createdAt); } } diff --git a/server/src/main/java/com/talkka/server/bus/service/BusLocationCollectService.java b/server/src/main/java/com/talkka/server/bus/service/BusLocationCollectService.java index 4a097df6..078e8886 100644 --- a/server/src/main/java/com/talkka/server/bus/service/BusLocationCollectService.java +++ b/server/src/main/java/com/talkka/server/bus/service/BusLocationCollectService.java @@ -2,4 +2,6 @@ public interface BusLocationCollectService { void collectLocations(); + + void collectLocationsByRouteId(String apiRouteId); } diff --git a/server/src/main/java/com/talkka/server/bus/service/BusLocationProcessor.java b/server/src/main/java/com/talkka/server/bus/service/BusLocationProcessor.java new file mode 100644 index 00000000..d1793321 --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/service/BusLocationProcessor.java @@ -0,0 +1,310 @@ +package com.talkka.server.bus.service; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.talkka.server.bus.dao.BusLocationEntity; +import com.talkka.server.bus.dao.BusPlateStatisticEntity; +import com.talkka.server.bus.dao.BusPlateStatisticRepository; +import com.talkka.server.bus.dao.BusRemainSeatEntity; +import com.talkka.server.bus.dao.BusRouteEntity; +import com.talkka.server.bus.dao.BusRouteRepository; +import com.talkka.server.bus.dao.BusRouteStationEntity; +import com.talkka.server.bus.dao.BusRouteStationRepository; +import com.talkka.server.bus.dao.BusStationEntity; +import com.talkka.server.bus.dao.BusStationRepository; +import com.talkka.server.bus.enums.PlateType; +import com.talkka.server.bus.exception.InvalidLocationNotFoundException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BusLocationProcessor { + private final BusRouteRepository busRouteRepository; + private final BusStationRepository busStationRepository; + private final BusRouteStationRepository busRouteStationRepository; + private final BusPlateStatisticRepository busPlateStatisticRepository; + private final Map> locationMap = new HashMap<>(); + private final Map> routeMap = new HashMap<>(); + private final Map> stationMap = new HashMap<>(); + private final Map> routeStationMap = new HashMap<>(); + private final Map routeLengthMap = new HashMap<>(); + private final List routeInfoList = new ArrayList<>(); + + @Transactional + public void start(List locations) { + init(); + for (var location : locations) { + if (location.getRemainSeatCount() == -1) { + continue; + } + try { + if (locationMap.containsKey(location.getPlateNo())) { + var dq = locationMap.get(location.getPlateNo()); + if (!dq.isEmpty() && location.getStationSeq() < dq.peekLast().getStationSeq()) { + process(dq); + dq = new ArrayDeque<>(); + locationMap.put(location.getPlateNo(), dq); + } + dq.add(location); + } else { + Deque dq = new ArrayDeque<>(); + dq.add(location); + locationMap.put(location.getPlateNo(), dq); + } + } catch (InvalidLocationNotFoundException e) { + log.warn("버스 위치정보 가공 실패 locationId : {}, apiRouteId : {}, apiStationId : {}, plateNo : {} ", + location.getBusLocationId(), + location.getApiRouteId(), + location.getApiStationId(), + location.getPlateNo()); + } + } + persist(); + } + + private void persist() { + for (String key : locationMap.keySet()) { + try { + var tmp = locationMap.get(key); + if (!tmp.isEmpty()) { + process(tmp); + } + } catch (InvalidLocationNotFoundException e) { + log.warn("버스 위치정보 가공 실패"); + } + } + busPlateStatisticRepository.saveAll(routeInfoList); + } + + /** + * 특정 버스(plateNo)를 기준으로 수집된 데이터를 처리합니다. + * 빠져있는 정거장 정보를 양 옆 정거장의 평균 정보로 대치하여 전체 노선 데이터를 완성하고, 이를 {@code routeInfoList}에 추가합니다. + * + * @param locations 특정 버스의 위치 정보를 담고 있는 {@code Deque}입니다. + * 이 정보는 특정 버스에 대한 다양한 정거장에서의 위치 데이터를 포함합니다. + */ + private void process(Deque locations) { + assert locations.peek() != null; + String apiRouteId = locations.peek().getApiRouteId(); + String apiStationId = locations.peek().getApiStationId(); + String plateNo = locations.peek().getPlateNo(); + LocalDateTime createdAt = locations.peek().getCreatedAt(); + List result = new ArrayList<>(); + try { + // 노선의 길이 확인 + var routeStationList = getRouteStationByRouteId(apiRouteId); + int num = routeStationList.size(); + // 노선 손실률이 80% 이상이면 저장하지 않음 + if (locations.size() < num * 0.2) { + return; + } + if (locations.peek().getStationSeq() != 1) { + result.add(makeSeatEntity(Optional.empty(), Optional.of(locations.peek()), 1, + routeStationList.get(0).getStation())); + } else { + result.add(toSeatEntity(locations.poll(), 1)); + } + for (int i = 2; i <= num; i++) { + BusLocationEntity cur = null; + // StationSeq 가 중복되면 마지막것을 선택 + while (!locations.isEmpty() && locations.peek().getStationSeq() == i) { + cur = locations.poll(); + } + // 특정 정거장의 위치정보가 없거나 좌석정보가 없으면 양옆 정류장 데이터의 평균으로 대치 + if (cur == null || cur.getRemainSeatCount() == -1) { + if (locations.isEmpty()) { + result.add(makeSeatEntity(Optional.of(result.get(i - 2)), Optional.empty(), i, + routeStationList.get(i - 1).getStation())); + } else { + result.add(makeSeatEntity(Optional.of(result.get(i - 2)), Optional.of(locations.peek()), i, + routeStationList.get(i - 1).getStation())); + } + } else { + result.add(toSeatEntity(cur, i)); + } + } + } catch (InvalidLocationNotFoundException e) { + log.warn("버스 위치정보 가공 실패 : 노선({}), 정거장({}), 버스이름({}), 생성일({})", + apiRouteId, apiStationId, plateNo, createdAt); + } + if (result.isEmpty()) { + return; + } + BusPlateStatisticEntity routeInfo = BusPlateStatisticEntity.builder() + .route(result.get(0).getRoute()) + .plateNo(result.get(0).getPlateNo()) + .plateType(result.get(0).getPlateType()) + .epochDay(result.get(0).getEpochDay()) + .startTime(result.get(0).getTime()) + .endTime(result.get(result.size() - 1).getTime()) + .seats(result) + .build(); + result.forEach(seat -> seat.updateRouteInfo(routeInfo)); + routeInfoList.add(routeInfo); + } + + private BusRemainSeatEntity toSeatEntity(BusLocationEntity location, int stationSeq) { + var route = getRoute(location.getApiRouteId()); + var station = getStation(location.getApiStationId()); + return BusRemainSeatEntity.build() + .route(route) + .station(station) + .stationSeq(stationSeq) + .emptySeat(location.getRemainSeatCount()) + .plateNo(location.getPlateNo()) + .plateType(location.getPlateType()) + .setCreateTime(location.getCreatedAt()) + .build(); + } + + /** + * 비어있는 엔티티가 있을 경우, 좌우 데이터를 비교하여 새로운 {@code BusRemainSeatEntity}를 생성합니다. + * + *

이 메서드는 이전 정거장과 이후 정거장의 데이터를 비교하여 평균값을 계산하거나, + * 기점/종점에 따른 특수 조건을 적용하여 새로운 좌석 정보를 생성합니다.

+ * + * @param before 이전 정거장의 좌석 정보. {@code Optional}로 제공되며, + * 비어있을 수도 있습니다. + * @param after 이후 정거장의 위치 정보. {@code Optional}로 제공되며, + * 비어있을 수도 있습니다. + * @param stationSeq 정거장의 순서 번호입니다. + * @param station 현재 정거장을 나타내는 {@code BusStationEntity}입니다. + * @return 새롭게 생성된 {@code BusRemainSeatEntity} 객체입니다. + * @throws InvalidLocationNotFoundException 이전 정거장과 이후 정거장 정보가 모두 없는 경우에 발생합니다. + */ + private BusRemainSeatEntity makeSeatEntity(Optional before, Optional after, + int stationSeq, BusStationEntity station) throws + InvalidLocationNotFoundException { + if (before.isPresent() && after.isPresent()) { + var beforeEntity = before.get(); + var afterEntity = after.get(); + var route = beforeEntity.getRoute(); + return BusRemainSeatEntity.build() + .route(route) + .station(station) + .stationSeq(stationSeq) + .emptySeat((beforeEntity.getEmptySeat() + afterEntity.getRemainSeatCount()) / 2) + .plateNo(beforeEntity.getPlateNo()) + .plateType(beforeEntity.getPlateType()) + .setCreateTime(getBetweenTime(beforeEntity.getCreatedAt(), afterEntity.getCreatedAt(), + afterEntity.getStationSeq() - beforeEntity.getStationSeq())) + .build(); + } else if (before.isPresent()) { + var beforeEntity = before.get(); + int emptySeat = beforeEntity.getEmptySeat(); + // 종점에서는 모든 자리가 비어있음 + if (stationSeq == getRouteLength(beforeEntity.getRoute().getApiRouteId())) { + if (beforeEntity.getPlateType() == PlateType.LARGE) { + emptySeat = 45; + } else { + emptySeat = 70; + } + } + var route = beforeEntity.getRoute(); + return BusRemainSeatEntity.build() + .route(route) + .station(station) + .stationSeq(stationSeq) + .emptySeat(emptySeat) + .plateNo(beforeEntity.getPlateNo()) + .plateType(beforeEntity.getPlateType()) + .setCreateTime(beforeEntity.getCreatedAt().plusMinutes(3)) + .build(); + } else if (after.isPresent()) { + var afterEntity = after.get(); + int emptySeat = afterEntity.getRemainSeatCount(); + // 기점에서는 모든 자리가 비어있음 + if (stationSeq == 1) { + if (afterEntity.getPlateType() == PlateType.LARGE) { + emptySeat = 45; + } else { + emptySeat = 70; + } + } + var route = getRoute(afterEntity.getApiRouteId()); + return BusRemainSeatEntity.build() + .route(route) + .station(station) + .stationSeq(stationSeq) + .emptySeat(emptySeat) + .plateNo(afterEntity.getPlateNo()) + .plateType(afterEntity.getPlateType()) + .setCreateTime(afterEntity.getCreatedAt().minusMinutes(3)) + .build(); + } else { + throw new InvalidLocationNotFoundException(); + } + } + + private BusRouteEntity getRoute(String apiRouteId) throws InvalidLocationNotFoundException { + if (!routeMap.containsKey(apiRouteId)) { + routeMap.put(apiRouteId, busRouteRepository.findByApiRouteId(apiRouteId)); + } + return routeMap.get(apiRouteId).orElseThrow(InvalidLocationNotFoundException::new); + } + + private BusStationEntity getStation(String apiStationId) throws InvalidLocationNotFoundException { + if (!stationMap.containsKey(apiStationId)) { + stationMap.put(apiStationId, busStationRepository.findByApiStationId(apiStationId)); + } + return stationMap.get(apiStationId).orElseThrow(InvalidLocationNotFoundException::new); + } + + private List getRouteStationByRouteId(String apiRouteId) { + var routeId = getRoute(apiRouteId).getId(); + if (!routeStationMap.containsKey(routeId)) { + var list = busRouteStationRepository.findAllByRouteId(routeId); + list.sort(Comparator.comparingInt(BusRouteStationEntity::getStationSeq)); + routeStationMap.put(routeId, list); + } + return routeStationMap.get(routeId); + } + + private Integer getRouteLength(String apiRouteId) throws InvalidLocationNotFoundException { + if (!routeLengthMap.containsKey(apiRouteId)) { + var route = getRoute(apiRouteId); + routeLengthMap.put(apiRouteId, busRouteStationRepository.countAllByRoute(route)); + } + return routeLengthMap.get(apiRouteId); + } + + private void init() { + for (var stationEntity : busStationRepository.findAll()) { + stationMap.put(stationEntity.getApiStationId(), Optional.of(stationEntity)); + } + for (var routeEntity : busRouteRepository.findAll()) { + routeMap.put(routeEntity.getApiRouteId(), Optional.of(routeEntity)); + } + } + + /** + * 주어진 두 시간 {@code time1}과 {@code time2} 사이의 구간을 주어진 개수 {@code num}만큼 나누어 + * 그중 첫 번째 구간의 시작 시간을 반환합니다. + * + * @param time1 시작 시간으로 사용할 {@code LocalDateTime}입니다. + * @param time2 종료 시간으로 사용할 {@code LocalDateTime}입니다. + * @param num 두 시간 사이의 구간을 나눌 개수입니다. + * @return 첫 번째 구간의 끝 시간에 해당하는 {@code LocalDateTime} 객체를 반환합니다. + */ + private LocalDateTime getBetweenTime(LocalDateTime time1, LocalDateTime time2, int num) { + num = num == 0 ? 1 : num; + Duration duration = Duration.between(time1, time2); + Duration halfDuration = duration.dividedBy(num); + return time1.plus(halfDuration); + } +} diff --git a/server/src/main/java/com/talkka/server/bus/service/BusLocationService.java b/server/src/main/java/com/talkka/server/bus/service/BusLocationService.java index ef37863f..b86d69a9 100644 --- a/server/src/main/java/com/talkka/server/bus/service/BusLocationService.java +++ b/server/src/main/java/com/talkka/server/bus/service/BusLocationService.java @@ -18,7 +18,7 @@ public class BusLocationService { private final BusLocationRepository busLocationRepository; @Transactional - public void saveBusLocations(List responseList, int apiCallNo, LocalDateTime createdAt) { + public void saveBusLocations(List responseList, Integer apiCallNo, LocalDateTime createdAt) { List entityList = responseList.stream() .map(dto -> dto.toEntity(apiCallNo, createdAt)) .toList(); diff --git a/server/src/main/java/com/talkka/server/bus/service/BusViewService.java b/server/src/main/java/com/talkka/server/bus/service/BusViewService.java new file mode 100644 index 00000000..0641f195 --- /dev/null +++ b/server/src/main/java/com/talkka/server/bus/service/BusViewService.java @@ -0,0 +1,115 @@ +package com.talkka.server.bus.service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.talkka.server.bus.dao.BusRemainSeatEntity; +import com.talkka.server.bus.dao.BusRemainSeatRepository; +import com.talkka.server.bus.dao.BusRouteStationEntity; +import com.talkka.server.bus.dao.BusRouteStationRepository; +import com.talkka.server.bus.dto.BusViewDto; +import com.talkka.server.bus.exception.BusRouteNotFoundException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class BusViewService { + private final BusRemainSeatRepository busRemainSeatRepository; + private final BusRouteStationRepository busRouteStationRepository; + + /** + * 버스 경로 정보를 조회합니다. + * + * @param routeStationId 기준 노선정거장 ID + * @param stationNum 현재 정거장에서 전후 몇 정거장을 조회할지 설정 + * ex) stationNum = 3 -> 기준 노선정거장 3개 전~3개 후 조회 + * @param time 기준 시간 + * @param timeRangeMinute 기준 시간 전후 몇 분까지 조회할지 설정 + * ex) timeRangeMinute = 3 -> 기준 시간 3분 전~3분 후 조회 + * @param week 몇 주 전의 데이터를 조회할지 설정 + * ex) week = 2 -> 2주 전 같은 요일의 데이터를 조회 + * @return BusRouteInfoRespDto 버스 경로 정보 + */ + public BusViewDto getBusView( + Long routeStationId, + Integer stationNum, + LocalDateTime time, + Integer timeRangeMinute, + Long week + ) { + // 타겟 노선정거장 조회 + var routeStation = busRouteStationRepository.findById(routeStationId) + .orElseThrow(() -> new BusRouteNotFoundException(routeStationId)); + Long routeId = routeStation.getRoute().getId(); + Long stationId = routeStation.getStation().getId(); + // 타겟 노선이 지나가는 모든 노선정거장 조회 + var routeStationList = busRouteStationRepository.findAllByRouteId(routeId) + .stream() + .filter(rs -> Math.abs(rs.getStationSeq() - routeStation.getStationSeq()) <= stationNum) + .sorted(Comparator.comparing(BusRouteStationEntity::getStationSeq)) + .toList(); + + // 새벽 3시까지는 전날로 침 + long epochDay = time.getHour() < 3 ? time.toLocalDate().toEpochDay() - 1 : time.toLocalDate().toEpochDay(); + var timeIntervals = getTimeInterval(time, timeRangeMinute); + + // 조회 조건에 맞는 버스 도착 정보 가져옴 + List busSeats = new ArrayList<>(); + for (var timeInterval : timeIntervals) { + busSeats.addAll( + busRemainSeatRepository.findByRouteIdAndStationIdAndEpochDayAndTimeBetween( + routeId, + stationId, + epochDay - 7 * week, + timeInterval[0], + timeInterval[1] + ) + ); + } + List> data = new ArrayList<>(); + for (var seat : busSeats) { + // 타겟 정거장 전후 도착정보 + data.add( + busRemainSeatRepository.findByPlateStatisticAndStationSeqBetweenOrderByStationSeq( + seat.getPlateStatistic(), + seat.getStationSeq() - stationNum, + seat.getStationSeq() + stationNum + ) + ); + } + return new BusViewDto( + time, + routeStationList, + data + ); + } + + /** + * 시간 구간이 0시를 넘어가는 경우, 두 개의 구간으로 나누어 검색을 수행합니다. + * 예를 들어, 23:43에서 00:13 사이의 시간 구간은 23:43-23:59 및 00:00-00:13 두 개의 구간으로 나누어집니다. + * + * @param time 기준 시간으로, 이 시간으로부터 {@code timeRange} 분 전후의 구간을 계산합니다. + * @param timeRange 기준 시간에서 검색할 분 단위의 시간 범위입니다. + * @return 시간 구간을 나타내는 {@code List}를 반환합니다. + * 각 배열은 [시작 시간, 종료 시간] 형식으로 시간 구간을 나타냅니다. + * 시간이 0시를 넘어가는 경우, 두 개의 구간으로 나누어 반환합니다. + */ + private List getTimeInterval(LocalDateTime time, Integer timeRange) { + List intervals = new ArrayList<>(); + int startTime = + time.minusMinutes(timeRange).getHour() * 100 + time.minusMinutes(timeRange).getMinute(); + int endTime = time.plusMinutes(timeRange).getHour() * 100 + time.plusMinutes(timeRange).getMinute(); + if (startTime < endTime) { + intervals.add(new Integer[] {startTime, endTime}); + } else { + intervals.add(new Integer[] {startTime, 2359}); + intervals.add(new Integer[] {0, endTime}); + } + return intervals; + } +} diff --git a/server/src/main/java/com/talkka/server/bus/service/LocationCollectingScheduler.java b/server/src/main/java/com/talkka/server/bus/service/LocationCollectingScheduler.java index be63636e..c1f0391b 100644 --- a/server/src/main/java/com/talkka/server/bus/service/LocationCollectingScheduler.java +++ b/server/src/main/java/com/talkka/server/bus/service/LocationCollectingScheduler.java @@ -1,13 +1,19 @@ package com.talkka.server.bus.service; import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import com.talkka.server.bus.util.BusLocationCollectProvider; import com.talkka.server.bus.util.LocationCollectingSchedulerConfigProperty; import lombok.extern.slf4j.Slf4j; @@ -18,6 +24,8 @@ public class LocationCollectingScheduler { private final LocationCollectingSchedulerConfigProperty locationCollectingSchedulerConfigProperty; private final BusLocationCollectService busLocationCollectService; + @Autowired + private BusLocationCollectProvider busLocationCollectProvider; public LocationCollectingScheduler( @Qualifier("locationCollectingSchedulerConfigProperty") @@ -29,14 +37,56 @@ public LocationCollectingScheduler( this.busLocationCollectService = busLocationCollectService; } - @Transactional + // 병렬 버스 위치 api 호출 메소드 @Scheduled(fixedRate = 1000 * 60) // per minute - public void runLocationScheduler() { + public void runParallelLocationScheduler() { if (isEnabled()) { - busLocationCollectService.collectLocations(); + List targetList = busLocationCollectProvider.getTargetIdList(); + ExecutorService executor = Executors.newFixedThreadPool(20); + + // CompletableFuture 리스트 생성 + List> futures = targetList.stream() + .map(targetId -> CompletableFuture.runAsync(() -> { + try { + busLocationCollectService.collectLocationsByRouteId(targetId); + } catch (Exception e) { + // 재시도 후에도 실패한 경우, 로그 남기기 + log.error(" 버스 위치 저장 실패 {} : {}", targetId, e.getMessage()); + } + }, executor)) + .toList(); + + // 모든 작업이 완료될 때까지 기다림 + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + // ExecutorService 종료 + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } } } + // 순차적 버스 위치 api 호출 메소드 + // @Scheduled(fixedRate = 1000 * 10) // per minute + // public void runLocationScheduler() { + // if (isEnabled()) { + // busLocationCollectProvider.getTargetIdList() + // .forEach(targetId -> { + // try { + // retryCollectLocations(targetId, 3); + // } catch (InterruptedException e) { + // log.error("{} : {}", targetId, e.getMessage()); + // } + // }); + // } + // } + private boolean isEnabled() { if (!locationCollectingSchedulerConfigProperty.isEnabled()) { return false; diff --git a/server/src/main/java/com/talkka/server/common/enums/TimeSlot.java b/server/src/main/java/com/talkka/server/common/enums/TimeSlot.java index e42b5f00..d5c9abb0 100644 --- a/server/src/main/java/com/talkka/server/common/enums/TimeSlot.java +++ b/server/src/main/java/com/talkka/server/common/enums/TimeSlot.java @@ -62,4 +62,4 @@ public static TimeSlot valueOfEnumString(String enumValue) { throw new InvalidTimeSlotEnumException(); } } -} +} \ No newline at end of file