Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 버스 교통편 조회 API #1099

Merged
merged 22 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f9c9d8d
feat : 도메인 모델 비스니스 구현 로직 작성
DHkimgit Nov 26, 2024
f62e322
feat : command DTO 작성
DHkimgit Nov 26, 2024
c138c80
feat : BusStation enum 멤버 추가
DHkimgit Nov 26, 2024
70e5b38
feat : repository query 메소드 추가
DHkimgit Nov 26, 2024
180f899
feat : 전략패턴 인터페이스 작성
DHkimgit Nov 26, 2024
8f487af
feat : 전략패턴 구현체 작성
DHkimgit Nov 26, 2024
fa05713
feat : response DTO 작성
DHkimgit Nov 26, 2024
ab7e344
feat : 시내버스 스케줄 조회 구현 로직 개선
DHkimgit Nov 26, 2024
cd81e3b
feat : 대성 버스 스케줄 데이터 작성
DHkimgit Nov 26, 2024
69ed6b8
feat : service 계층 작성
DHkimgit Nov 26, 2024
e8f80cf
feat : controller 작성
DHkimgit Nov 26, 2024
552b763
chore : 클래스 이름 변경
DHkimgit Nov 26, 2024
65a66b9
refactor : 셔틀버스 구현체 통합
DHkimgit Nov 26, 2024
fca0a90
chore : 출력 수정, 상수 추가
DHkimgit Nov 26, 2024
bb908b5
chore : static import 적용
DHkimgit Nov 28, 2024
a8646e4
chore : 코드 리뷰 반영 - indent
DHkimgit Nov 28, 2024
475534c
feat : 버스 타입 정렬 기준 추가
DHkimgit Dec 6, 2024
ef1ef9e
feat : Api 인터페이스 메서드 추가
DHkimgit Dec 6, 2024
187fbd2
chore : 주석, 스키마 수정 및 개선
DHkimgit Dec 10, 2024
0552b56
chore : 주석, 스키마 수정 및 개선
DHkimgit Dec 10, 2024
031e938
fix : 일부 요청에 대해 404 에러 반환하는 오류 수정
DHkimgit Dec 11, 2024
2c4291b
feat : 천안역 <-> 터미널 정보 제공
DHkimgit Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 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,30 @@ 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 = """
### 버스 교통편 조회
- **시간** : 00:00 인 경우 해당 날짜의 모든 스케줄을 조회합니다.
- **날짜** : 요일을 기준으로 스케줄을 출력합니다. 공휴일 처리는 구현되어 있지 않습니다.
- **출발지 & 도착지** : 출발지와 도착지가 일치하는 경우 빈 리스트를 반환합니다. (천안역 -> 터미널) & (터미널 -> 천안역) 역시 빈 리스트를 반환합니다.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R

터미널, 천안역 간 교통편 조회는 빈 결과를 반환한다는 요구사항이 있었나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 데이터 중 터미널-천안역 간 정확한 스케줄 정보를 제공할 수 있는 데이터가 없어서 빈 결과를 반환하게 했습니다. 따로 제시된 요구사항은 없습니다. 천안역 -> 천안역이 빈 리스트를 반환하는 것과 같은 맥락으로 생각했습니다. PM님과 상의해야하는 부분일까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 회색지대인 요구사항이 있다면 임의로 구현하기보다는 PM님과 팀에서 논의하는게 좋을 것 같아요~
논의 올려주신내용 확인했습니다

"""
)
@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,
Comment on lines +114 to +115
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C

request와 response에서 모두 date와 time을 분리하여 전달하고 있는데, 분리해야 했던 이유가 있을까요?? 다른 곳에서는 대부분은 합쳐서 사용하고 있지 않나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디자인에 '오늘 오후 10:30 출발' 이런식으로 출력하도록 되어 있길래 분리 하는 게 클라이언트 입장에서 더 편할 것 같다고 생각했습니다. 버스 도메인 다른 API(버스 검색)도 저런 형태로 정보를 받고 있어서 그대로 진행했습니다.

@Parameter(
description = "CITY, EXPRESS, SHUTTLE, ALL"
) @RequestParam BusRouteType busRouteType,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

파라미터 타입으로 enum을 주면 swagger에서 조회 시 드롭다운으로 나타날텐데 description에 예시를 제공해야 할까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요하네요! 제거하겠습니다.

@Parameter(description = "KOREATECH, TERMINAL, STATION") @RequestParam BusStation depart,
@Parameter(description = "KOREATECH, TERMINAL, STATION") @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);
}
}
Copy link
Collaborator

@songsunkook songsunkook Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C

request dto 용도의 클래스를 만들어야 한다면 차라리 GET요청이지만 request body를 사용하는 건 어떻게 생각하시나요?? 기능에 비해 복잡도가 커지는 것 같아서 우려되네요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BusRouteCommand를 request dto처럼 만드는게 어떻냐는 말씀이신가요? command 객체가 BusRouteStrategy 추상화 메소드의 매개변수 타입으로 사용되고 있는데 이 객체를 request dto처럼 만들면 더 복잡해 질 것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파라미터로 받은 내용들을 다시 전부 단일 record로 묶어서 service를 호출하길래 get 요청에 body를 담지 않으려고 이렇게 한건가? 했습니다. 그런 구조였다면 괜찮을 수도 있을 것 같네요 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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() {
if (depart == arrive) return false;
if (depart == BusStation.STATION && arrive == BusStation.TERMINAL) return false;
if (depart == BusStation.TERMINAL && arrive == BusStation.STATION) return false;
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 java.util.stream.Collectors;

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 = "교통편 조회 결과", example = """
[
{
"bus_type" : "express",
"route_name" : "대성티앤이",
"depart_time" : "16:50"
},
{
"bus_type" : "city",
"route_name" : "400",
"depart_time" : "16:56"
},
{
"bus_type" : "city",
"route_name" : "402",
"depart_time" : "17:30"
},
{
"bus_type" : "shuttle",
"route_name" : "주중(20시 00분)",
"depart_time" : "20:00"
}
]
""", requiredMode = NOT_REQUIRED)
List<ScheduleInfo> schedule

) {
@JsonNaming(SnakeCaseStrategy.class)
public record ScheduleInfo(
String busType,
String busName,
LocalTime departTime
) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

여기도 @Schema로 부연설명을 붙일 수 있을 것 같아요. 그렇게 한다면 schedule 변수의 Schema 예시 설명을 제거할 수 있을 것 같습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니다.


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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

주석으로 부연설명을 조금 달아두는 것도 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

적용했습니다.

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

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

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,11 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CityBusTimetable {

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;
private static final Integer ADDITIONAL_TIME_DEPART_TO_STATION = 7;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

기점에서 정류장까지 걸리는 시간(분)인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다! 시내버스 API 에서 받아오는 출발 시간 데이터는 기점 출발 기준입니다. 예를 들어 400번의 경우 한기대 정류장이 아니라 출발지인 병천3리 정류장을 기준으로 스케줄 정보를 제공합니다. 따라서 사용자가 한기대 -> 터미널의 스케줄을 요청한 경우 시간 데이터에 병천3리 -> 한기대 사이의 이동 시간을 추가하여 시간을 보정합니다. 해당 작업을 위해 정의한 상수입니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

해당 정보의 출처는 어디인지, 이 상수들의 용도가 무엇인지 주석으로 명시하면 좋을 것 같아요!


@Id
@Field("_id")
private String routeId;
Expand All @@ -38,6 +49,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 +91,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
Loading