Skip to content

Commit

Permalink
feat: 버스 시간표 불러오기 (#369)
Browse files Browse the repository at this point in the history
* feat : 인수인계용 커밋

* feat : ExpressBusTimeTable record 삭제

* feat : ExpressBusTimetable 클래스 생성

* feat : ExpressBusOpenApiClient 클래스 완성

* feat : ExpressBusCache busInfos 생성자 다시 추가

* feat : busType.Lowercase 추가

* feat : 미사용 쿼리 제거, getBy로 변경

* feat : 테스트 추가

* feat : 리뷰 반영
  • Loading branch information
dradnats1012 authored Apr 10, 2024
1 parent a29be69 commit fa25f90
Show file tree
Hide file tree
Showing 13 changed files with 340 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import in.koreatech.koin.domain.bus.dto.BusCourseResponse;
import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse;
import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse;
import in.koreatech.koin.domain.bus.model.BusTimetable;
import in.koreatech.koin.domain.bus.model.enums.BusStation;
import in.koreatech.koin.domain.bus.model.enums.BusType;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -39,6 +40,14 @@ ResponseEntity<BusRemainTimeResponse> getBusRemainTime(
@Parameter(description = "koreatech, station, terminal") @RequestParam BusStation arrival
);

@Operation(summary = "버스 시간표 조회")
@GetMapping("/timetable")
ResponseEntity<List<? extends BusTimetable>> getBusTimetable(
@RequestParam(value = "bus_type") BusType busType,
@RequestParam(value = "direction") String direction,
@RequestParam(value = "region") String region
);

@ApiResponses(
value = {
@ApiResponse(responseCode = "200"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import in.koreatech.koin.domain.bus.dto.BusCourseResponse;
import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse;
import in.koreatech.koin.domain.bus.model.BusTimetable;
import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse;
import in.koreatech.koin.domain.bus.model.enums.BusStation;
import in.koreatech.koin.domain.bus.model.enums.BusType;
Expand All @@ -36,6 +37,15 @@ public ResponseEntity<BusRemainTimeResponse> getBusRemainTime(
return ResponseEntity.ok().body(busRemainTime);
}

@GetMapping("/timetable")
public ResponseEntity<List<? extends BusTimetable>> getBusTimetable(
@RequestParam(value = "bus_type") BusType busType,
@RequestParam(value = "direction") String direction,
@RequestParam(value = "region") String region
){
return ResponseEntity.ok().body(busService.getBusTimetable(busType, direction, region));
}

@GetMapping("/courses")
public ResponseEntity<List<BusCourseResponse>> getBusCourses() {
return ResponseEntity.ok().body(busService.getBusCourses());
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package in.koreatech.koin.domain.bus.exception;

public class BusTypeNotSupportException extends IllegalArgumentException {
private static final String DEFAULT_MESSAGE = "해당 버스타입에는 지원하지 않는 기능입니다.";

public BusTypeNotSupportException(String message) {
super(message);
}

public static BusTypeNotSupportException withDetail(String detail) {
String message = String.format("%s %s", DEFAULT_MESSAGE, detail);
return new BusTypeNotSupportException(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package in.koreatech.koin.domain.bus.model;

public abstract class BusTimetable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package in.koreatech.koin.domain.bus.model;

import java.util.List;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import lombok.Getter;

@Getter
@JsonNaming(value = SnakeCaseStrategy.class)
public class SchoolBusTimetable extends BusTimetable {
private final String routeName;
private final List<ArrivalNode> arrivalInfo;

public SchoolBusTimetable(String routeName, List<ArrivalNode> arrivalInfo){
this.routeName = routeName;
this.arrivalInfo = arrivalInfo;
}

@Getter
public static class ArrivalNode {
private final String nodeName;
private final String arrivalTime;

public ArrivalNode(String nodeName, String arrivalTime){
this.nodeName = nodeName;
this.arrivalTime = arrivalTime;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
CityBusRemainTime that = (CityBusRemainTime) o;
CityBusRemainTime that = (CityBusRemainTime)o;
return Objects.equals(getBusArrivalTime(), that.getBusArrivalTime())
&& Objects.equals(busNumber, that.busNumber);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import java.time.LocalTime;

public record ExpressBusCacheInfo(
LocalTime departureTime,
LocalTime arrivalTime,
LocalTime depart,
LocalTime arrival,
int charge
) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package in.koreatech.koin.domain.bus.model.express;

import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import in.koreatech.koin.domain.bus.model.BusTimetable;
import lombok.Getter;

@Getter
@JsonNaming(value = SnakeCaseStrategy.class)
public class ExpressBusTimetable extends BusTimetable {

private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");

private final String depart;

private final String arrival;

private final int charge;

public ExpressBusTimetable(String depart, String arrival, int charge){
this.depart = depart;
this.arrival = arrival;
this.charge = charge;
}

public static ExpressBusTimetable from(ExpressBusCacheInfo expressBusCacheInfo){
String departure = expressBusCacheInfo.depart().format(TIME_FORMATTER);
String arrival = expressBusCacheInfo.arrival().format(TIME_FORMATTER);
int charge = expressBusCacheInfo.charge();
return new ExpressBusTimetable(departure, arrival, charge);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package in.koreatech.koin.domain.bus.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.repository.Repository;

import in.koreatech.koin.domain.bus.exception.BusNotFoundException;
import in.koreatech.koin.domain.bus.model.mongo.BusCourse;

public interface BusRepository extends Repository<BusCourse, String> {
Expand All @@ -13,4 +15,11 @@ public interface BusRepository extends Repository<BusCourse, String> {
List<BusCourse> findAll();

List<BusCourse> findByBusType(String busType);

Optional<BusCourse> findByBusTypeAndDirectionAndRegion(String busType, String direction, String region);

default BusCourse getByBusTypeAndDirectionAndRegion(String busType, String direction, String region) {
return findByBusTypeAndDirectionAndRegion(busType, direction, region).orElseThrow(
() -> BusNotFoundException.withDetail("region"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse;
import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse;
import in.koreatech.koin.domain.bus.exception.BusIllegalStationException;
import in.koreatech.koin.domain.bus.exception.BusTypeNotFoundException;
import in.koreatech.koin.domain.bus.exception.BusTypeNotSupportException;
import in.koreatech.koin.domain.bus.model.BusRemainTime;
import in.koreatech.koin.domain.bus.model.BusTimetable;
import in.koreatech.koin.domain.bus.model.SchoolBusTimetable;
import in.koreatech.koin.domain.bus.model.enums.BusDirection;
import in.koreatech.koin.domain.bus.model.enums.BusStation;
import in.koreatech.koin.domain.bus.model.enums.BusType;
Expand Down Expand Up @@ -149,10 +153,34 @@ private void validateBusCourse(BusStation depart, BusStation arrival) {
}
}

public List<? extends BusTimetable> getBusTimetable(BusType busType, String direction, String region) {
if (busType == BusType.CITY) {
throw new BusTypeNotSupportException("CITY");
}

if (busType == BusType.EXPRESS) {
return expressBusOpenApiClient.getExpressBusTimetable(direction);
}

if (busType == BusType.SHUTTLE || busType == BusType.COMMUTING) {
BusCourse busCourse = busRepository
.getByBusTypeAndDirectionAndRegion(busType.name().toLowerCase(), direction, region);

return busCourse.getRoutes().stream()
.map(route -> new SchoolBusTimetable(
route.getRouteName(),
route.getArrivalInfos().stream()
.map(node -> new SchoolBusTimetable.ArrivalNode(
node.getNodeName(), node.getArrivalTime())
).toList())).toList();
}

throw new BusTypeNotFoundException(busType.name());
}

public List<BusCourseResponse> getBusCourses() {
return busRepository.findAll().stream()
.map(BusCourseResponse::from)
.toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@
import com.google.gson.reflect.TypeToken;

import in.koreatech.koin.domain.bus.dto.ExpressBusRemainTime;
import in.koreatech.koin.domain.bus.dto.ExpressBusTimeTable;
import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse;
import in.koreatech.koin.domain.bus.exception.BusOpenApiException;
import in.koreatech.koin.domain.bus.model.BusRemainTime;
import in.koreatech.koin.domain.bus.model.BusTimetable;
import in.koreatech.koin.domain.bus.model.enums.BusOpenApiResultCode;
import in.koreatech.koin.domain.bus.model.enums.BusStation;
import in.koreatech.koin.domain.bus.model.express.ExpressBusCache;
import in.koreatech.koin.domain.bus.model.express.ExpressBusCacheInfo;
import in.koreatech.koin.domain.bus.model.express.ExpressBusRoute;
import in.koreatech.koin.domain.bus.model.express.ExpressBusStationNode;
import in.koreatech.koin.domain.bus.model.express.ExpressBusTimetable;
import in.koreatech.koin.domain.bus.model.express.OpenApiExpressBusArrival;
import in.koreatech.koin.domain.bus.repository.ExpressBusCacheRepository;
import in.koreatech.koin.domain.version.model.Version;
Expand Down Expand Up @@ -110,24 +111,26 @@ public List<ExpressBusRemainTime> getBusRemainTime(BusStation depart, BusStation
private void storeRemainTimeByOpenApi(String departName, String arrivalName) {
JsonObject busApiResponse = getBusApiResponse(departName, arrivalName);
List<OpenApiExpressBusArrival> busArrivals = extractBusArrivalInfo(busApiResponse);
expressBusCacheRepository.save(
ExpressBusCache.of(
new ExpressBusRoute(departName, arrivalName),
// API로 받은 yyyyMMddHHmm 형태의 시간을 HH:mm 형태로 변환하여 Redis에 저장한다.
busArrivals.stream()
.map(it -> new ExpressBusCacheInfo(
LocalTime.parse(
LocalDateTime.parse(it.depPlandTime(), ofPattern("yyyyMMddHHmm"))
.format(ofPattern("HH:mm"))
),
LocalTime.parse(
LocalDateTime.parse(it.arrPlandTime(), ofPattern("yyyyMMddHHmm"))
.format(ofPattern("HH:mm"))
),
it.charge()
))
.toList()
));

ExpressBusCache expressBusCache = ExpressBusCache.of(
new ExpressBusRoute(departName, arrivalName),
// API로 받은 yyyyMMddHHmm 형태의 시간을 HH:mm 형태로 변환하여 Redis에 저장한다.
busArrivals.stream()
.map(it -> new ExpressBusCacheInfo(
LocalTime.parse(
LocalDateTime.parse(it.depPlandTime(), ofPattern("yyyyMMddHHmm"))
.format(ofPattern("HH:mm"))
),
LocalTime.parse(
LocalDateTime.parse(it.arrPlandTime(), ofPattern("yyyyMMddHHmm"))
.format(ofPattern("HH:mm"))
),
it.charge()
))
.toList()
);

expressBusCacheRepository.save(expressBusCache);
versionRepository.getByType(VersionType.EXPRESS).update(clock);
}

Expand Down Expand Up @@ -162,7 +165,7 @@ private URL getBusApiURL(String depart, String arrival) {
ExpressBusStationNode arrivalNode = ExpressBusStationNode.from(arrival);
StringBuilder urlBuilder = new StringBuilder(OPEN_API_URL); /*URL*/
try {
urlBuilder.append("?" + encode("serviceKey", UTF_8) + "=" + openApiKey);
urlBuilder.append("?" + encode("serviceKey", UTF_8) + "=" + encode(openApiKey, UTF_8));
urlBuilder.append("&" + encode("numOfRows", UTF_8) + "=" + encode("30", UTF_8));
urlBuilder.append("&" + encode("_type", UTF_8) + "=" + encode("json", UTF_8));
urlBuilder.append("&" + encode("depTerminalId", UTF_8) + "=" + encode(departNode.getStationId(), UTF_8));
Expand Down Expand Up @@ -204,15 +207,11 @@ private List<ExpressBusRemainTime> getStoredRemainTime(String departName, String
return Collections.emptyList();
}
List<ExpressBusCacheInfo> busArrivals = expressBusCache.getBusInfos();
return getExpressBusRemainTime(
busArrivals
.stream()
.map(ExpressBusTimeTable::from)
.toList());
return getExpressBusRemainTime(busArrivals);
}

private List<ExpressBusRemainTime> getExpressBusRemainTime(
List<ExpressBusTimeTable> busArrivals
List<ExpressBusCacheInfo> busArrivals
) {
return busArrivals.stream()
.map(it -> new ExpressBusRemainTime(it.depart(), EXPRESS.name().toLowerCase()))
Expand All @@ -224,4 +223,37 @@ public boolean isCacheExpired(Version version, Clock clock) {
return duration.toSeconds() < 0
|| Duration.ofHours(ExpressBusCache.getCacheExpireHour()).toSeconds() <= duration.toSeconds();
}

public List<? extends BusTimetable> getExpressBusTimetable(String direction) {
Version version = versionRepository.getByType(VersionType.EXPRESS);
String depart = "", arrival = "";

if ("from".equals(direction)) {
depart = "koreatech";
arrival = "terminal";
}
if ("to".equals(direction)) {
depart = "terminal";
arrival = "koreatech";
}
if (depart.isEmpty() || arrival.isEmpty()) {
throw new UnsupportedOperationException();
}

if (isCacheExpired(version, clock)) {
storeRemainTimeByOpenApi(depart, arrival);
}

String busCacheId = ExpressBusCache.generateId(new ExpressBusRoute(depart, arrival));
ExpressBusCache expressBusCache = expressBusCacheRepository.getById(busCacheId);
if (Objects.isNull(expressBusCache)) {
return Collections.emptyList();
}
List<ExpressBusCacheInfo> busArrivals = expressBusCache.getBusInfos();

return busArrivals
.stream()
.map(ExpressBusTimetable::from)
.toList();
}
}
Loading

0 comments on commit fa25f90

Please sign in to comment.