Skip to content

Commit

Permalink
feat: 버스 교통편 조회 API
Browse files Browse the repository at this point in the history
feat: 버스 교통편 조회 API
  • Loading branch information
DHkimgit authored Dec 12, 2024
2 parents bc82893 + 2c4291b commit 392dfab
Show file tree
Hide file tree
Showing 17 changed files with 539 additions and 5 deletions.
27 changes: 27 additions & 0 deletions src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
import java.time.LocalDate;
import java.util.List;

import org.springframework.format.annotation.DateTimeFormat;
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 in.koreatech.koin.domain.bus.dto.BusCourseResponse;
import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse;
import in.koreatech.koin.domain.bus.dto.BusScheduleResponse;
import in.koreatech.koin.domain.bus.dto.BusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.CityBusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse;
import in.koreatech.koin.domain.bus.model.BusTimetable;
import in.koreatech.koin.domain.bus.model.enums.BusRouteType;
import in.koreatech.koin.domain.bus.model.enums.BusStation;
import in.koreatech.koin.domain.bus.model.enums.BusType;
import in.koreatech.koin.domain.bus.model.enums.CityBusDirection;
Expand Down Expand Up @@ -90,4 +93,28 @@ ResponseEntity<List<SingleBusTimeResponse>> getSearchTimetable(
@Operation(summary = "버스 노선 조회")
@GetMapping("/courses")
ResponseEntity<List<BusCourseResponse>> getBusCourses();

@ApiResponses(
value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
}
)
@Operation(
summary = "버스 교통편 조회",
description = """
### 버스 교통편 조회
- **시간** : 13:00 인 경우 13시 이후 출발하는 버스의 시간표를 조회합니다. 00:00 인 경우 해당 날짜의 모든 스케줄을 조회합니다.
- **날짜** : 요일을 기준으로 스케줄을 출력합니다. 공휴일 처리는 구현되어 있지 않습니다.
- **출발지 & 도착지** : 출발지와 도착지가 일치하는 경우 빈 리스트를 반환합니다.
"""
)
@GetMapping("/route")
ResponseEntity<BusScheduleResponse> getBusRouteSchedule(
@Parameter(description = "yyyy-MM-dd") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
@Parameter(description = "HH:mm") @RequestParam String time,
@RequestParam BusRouteType busRouteType,
@RequestParam BusStation depart,
@RequestParam BusStation arrival
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@

import in.koreatech.koin.domain.bus.dto.BusCourseResponse;
import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse;
import in.koreatech.koin.domain.bus.dto.BusRouteCommand;
import in.koreatech.koin.domain.bus.dto.BusScheduleResponse;
import in.koreatech.koin.domain.bus.dto.BusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.CityBusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse;
import in.koreatech.koin.domain.bus.model.BusTimetable;
import in.koreatech.koin.domain.bus.model.enums.BusRouteType;
import in.koreatech.koin.domain.bus.model.enums.BusStation;
import in.koreatech.koin.domain.bus.model.enums.BusType;
import in.koreatech.koin.domain.bus.model.enums.CityBusDirection;
Expand Down Expand Up @@ -82,4 +85,17 @@ public ResponseEntity<List<SingleBusTimeResponse>> getSearchTimetable(
depart, arrival);
return ResponseEntity.ok().body(singleBusTimeResponses);
}

@GetMapping("/route")
public ResponseEntity<BusScheduleResponse> getBusRouteSchedule(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
@RequestParam String time,
@RequestParam(value = "bus_type") BusRouteType busRouteType,
@RequestParam BusStation depart,
@RequestParam BusStation arrival
) {
BusRouteCommand request = new BusRouteCommand(depart, arrival, busRouteType, date, LocalTime.parse(time));
BusScheduleResponse busSchedule = busService.getBusSchedule(request);
return ResponseEntity.ok().body(busSchedule);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package in.koreatech.koin.domain.bus.dto;

import java.time.LocalDate;
import java.time.LocalTime;

import in.koreatech.koin.domain.bus.model.enums.BusRouteType;
import in.koreatech.koin.domain.bus.model.enums.BusStation;

public record BusRouteCommand(

BusStation depart,
BusStation arrive,
BusRouteType busRouteType,
LocalDate date,
LocalTime time
) {

public boolean checkAvailableCourse() {
return depart != arrive;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package in.koreatech.koin.domain.bus.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Comparator;
import java.util.List;

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

import in.koreatech.koin.domain.bus.model.enums.BusStation;
import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(SnakeCaseStrategy.class)
public record BusScheduleResponse(
@Schema(description = "출발 정류장", example = "KOREATECH", requiredMode = REQUIRED)
BusStation depart,
@Schema(description = "도착 정류장", example = "TERMINAL", requiredMode = REQUIRED)
BusStation arrival,
@Schema(description = "출발 날짜", example = "2024-11-05", requiredMode = REQUIRED)
LocalDate departDate,
@Schema(description = "출발 시간", example = "12:00", requiredMode = REQUIRED)
LocalTime departTime,
@Schema(description = "교통편 조회 결과", requiredMode = NOT_REQUIRED)
List<ScheduleInfo> schedule

) {
@JsonNaming(SnakeCaseStrategy.class)
public record ScheduleInfo(
@Schema(description = "버스 타입 (shuttle, express, city)", example = "express", requiredMode = REQUIRED)
String busType,
@Schema(description = "버스 이름 또는 노선명", example = "대성티앤이", requiredMode = REQUIRED)
String busName,
@Schema(description = "버스 출발 시간", example = "16:50", requiredMode = REQUIRED)
LocalTime departTime
) {

public static Comparator<ScheduleInfo> compareBusType() {
List<String> priority = List.of("shuttle", "express", "city");
return Comparator.comparingInt(schedule -> priority.indexOf(schedule.busType));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package in.koreatech.koin.domain.bus.model.enums;

import java.util.Arrays;

import com.fasterxml.jackson.annotation.JsonCreator;

import in.koreatech.koin.domain.bus.exception.BusTypeNotFoundException;

public enum BusRouteType {
CITY,
EXPRESS,
SHUTTLE,
ALL;

@JsonCreator
public static BusRouteType from(String busRouteTypeName) {
return Arrays.stream(values())
.filter(busType -> busType.name().equalsIgnoreCase(busRouteTypeName))
.findAny()
.orElseThrow(() -> BusTypeNotFoundException.withDetail("busRouteTypeName: " + busRouteTypeName));
}

public String getName() {
return this.name().toLowerCase();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@

@Getter
public enum BusStation {
KOREATECH(List.of("학교", "한기대", "코리아텍"), BusStationNode.KOREATECH),
STATION(List.of("천안역", "천안역(학화호두과자)"), BusStationNode.STATION),
TERMINAL(List.of("터미널", "터미널(신세계 앞 횡단보도)", "야우리"), BusStationNode.TERMINAL),
KOREATECH(List.of("학교", "한기대", "코리아텍"), BusStationNode.KOREATECH, "한기대"),
STATION(List.of("천안역", "천안역(학화호두과자)"), BusStationNode.STATION, "천안역"),
TERMINAL(List.of("터미널", "터미널(신세계 앞 횡단보도)", "야우리"), BusStationNode.TERMINAL, "터미널"),
;

private final List<String> displayNames;
private final BusStationNode node;
private final String queryName;

BusStation(List<String> displayNames, BusStationNode node) {
BusStation(List<String> displayNames, BusStationNode node, String queryName) {
this.displayNames = displayNames;
this.node = node;
this.queryName = queryName;
}

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

import java.time.LocalTime;
import java.util.List;

/**
* 한기대와 천안터미널 사이를 운행하는 대성 고속버스의 운행 스케줄을 정적인 데이터로 저장한 클래스입니다.
* 외부 API 가 동작하지 않는 이슈의 해결 전까지 임시적으로 사용하기 위해 작성되었습니다.
*/
public final class ExpressBusSchedule {

/**
* 천안 터미널 -> 한기대 출발 시간
*/
private static final List<LocalTime> KOREA_TECH_SCHEDULE = List.of(
LocalTime.of(7, 0),
LocalTime.of(8, 30),
LocalTime.of(9, 0),
LocalTime.of(10, 0),
LocalTime.of(12, 0),
LocalTime.of(12, 30),
LocalTime.of(13, 0),
LocalTime.of(15, 0),
LocalTime.of(16, 0),
LocalTime.of(16, 40),
LocalTime.of(18, 0),
LocalTime.of(19, 30),
LocalTime.of(20, 30)
);

/**
* 한기대 -> 천안 터미널 출발 시간
*/
private static final List<LocalTime> TERMINAL_SCHEDULE = List.of(
LocalTime.of(8, 35),
LocalTime.of(10, 35),
LocalTime.of(11, 5),
LocalTime.of(11, 35),
LocalTime.of(13, 35),
LocalTime.of(14, 35),
LocalTime.of(15, 5),
LocalTime.of(16, 35),
LocalTime.of(17, 35),
LocalTime.of(19, 5),
LocalTime.of(19, 35),
LocalTime.of(21, 5),
LocalTime.of(22, 5)
);

public static List<LocalTime> getExpressBusScheduleToKoreaTech() {
return KOREA_TECH_SCHEDULE;
}

public static List<LocalTime> getExpressBusScheduleToTerminal() {
return TERMINAL_SCHEDULE;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package in.koreatech.koin.domain.bus.model.mongo;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo;
import in.koreatech.koin.domain.bus.model.enums.BusStation;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
Expand All @@ -18,6 +24,20 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CityBusTimetable {

/**
* 1. 한기대 -> 터미널행 시내버스의 기점은 한기대 정류장이 아님
* 2. 400번은 병천3리, 402번은 황사동, 405번은 유관순열사사적지에서 출발해서 한기대 정류장에 도착
* 3. 제공받는 시내버스의 운행 시간은 기점 기준이기 때문에 각 버스의 기점에서 한기대 정류장까지 이동 시간을 더해서 보정해야 함
*/
private static final Integer ADDITIONAL_TIME_DEPART_TO_KOREATECH_400 = 6;
private static final Integer ADDITIONAL_TIME_DEPART_TO_KOREATECH_402 = 13;
private static final Integer ADDITIONAL_TIME_DEPART_TO_KOREATECH_405 = 7;
/**
* 1. 천안역 -> 한기대행 시내버스의 기점은 천안 터미널 정류장.
* 2. 제공받는 시내버스의 운행 시간은 기점 기준이기 때문에 터미널에서 천안역 정류장까지 이동 시간을 더해서 보정해야 함
*/
private static final Integer ADDITIONAL_TIME_DEPART_TO_STATION = 7;

@Id
@Field("_id")
private String routeId;
Expand All @@ -38,6 +58,15 @@ private CityBusTimetable(BusInfo busInfo, List<BusTimetable> busTimetables, Loca
this.updatedAt = updatedAt;
}

public List<ScheduleInfo> getScheduleInfo(LocalDate date, BusStation depart) {
Long busNumber = busInfo.getNumber();
return busTimetables.stream()
.filter(busTimetable -> busTimetable.filterByDayOfWeek(date))
.flatMap(busTimetable -> busTimetable.applyTimeOffset(busNumber, depart).stream()
.map(time -> new ScheduleInfo("city", busNumber.toString(), time)))
.collect(Collectors.toList());
}

@Getter
public static class BusInfo {

Expand Down Expand Up @@ -71,5 +100,31 @@ private BusTimetable(String dayOfWeek, List<String> departInfo) {
this.dayOfWeek = dayOfWeek;
this.departInfo = departInfo;
}

public boolean filterByDayOfWeek(LocalDate date) {
return switch (dayOfWeek) {
case "평일" -> date.getDayOfWeek() != DayOfWeek.SATURDAY && date.getDayOfWeek() != DayOfWeek.SUNDAY;
case "주말" -> date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;
default -> false;
};
}

public List<LocalTime> applyTimeOffset(Long busNumber, BusStation depart) {
return departInfo.stream()
.map(time -> {
LocalTime schedule = LocalTime.parse(time);
if (busNumber == 400 && depart == BusStation.KOREATECH) {
schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_KOREATECH_400);
} else if (busNumber == 402 && depart == BusStation.KOREATECH) {
schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_KOREATECH_402);
} else if (busNumber == 405 && depart == BusStation.KOREATECH) {
schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_KOREATECH_405);
} else if (depart == BusStation.STATION) {
schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_STATION);
}
return schedule;
})
.collect(Collectors.toList());
}
}
}
Loading

0 comments on commit 392dfab

Please sign in to comment.