Skip to content

Commit

Permalink
feat: 학교버스 시간표 관련 API (#1119)
Browse files Browse the repository at this point in the history
* feat: 학교버스 시간표 관련 API 2개 구현

/courses/shuttle 학교버스 노선 조회 API 구현
/timetable/shuttle 학교버스 시간표 조회 API 구현

* feat: 학교버스 노선 조회 response 스웨거 명세

* chore: 메서드 이름 변경

* feat: 학교버스 노선 조회 DTO 분리

모델 객체와 DTO 분리
컨트롤러 메서드 이름 변경

* feat: 학기 종류에 따른 노선 반환하도록 변경

정규학기, 계절학기, 방학에 따라 다른 노선 반환

* chore: response 스웨거 명세 정보 수정

* feat: response 구조 변경

전체 노선 조회 response 구조 변경

* feat: response 순서대로 반환

지역 순서대로 반환 (천안, 서울, 청주)
노선 종류 순서대로 반환 (순환, 주중, 주말)

* refactor: 리뷰 반영

uri 수정: /bus/timetable/shuttle/{id}
ShuttleBusService 구현
dto 책임 분리
dto 이름 변경

* refactor: enum 도입

지역, 노선종류 속성 enum 대체
정렬 로직 개선

* refactor: dto 변환 책임 분리 from service

* chore: detail 속성 추가

노선 정보에 detail 속성 추가
노선 지역 response 이름 수정
  • Loading branch information
kih1015 authored Dec 12, 2024
1 parent 610fee0 commit 91f7692
Show file tree
Hide file tree
Showing 11 changed files with 412 additions and 0 deletions.
22 changes: 22 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 @@ -6,6 +6,7 @@
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

Expand All @@ -14,6 +15,8 @@
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.ShuttleBusRoutesResponse;
import in.koreatech.koin.domain.bus.dto.ShuttleBusTimetableResponse;
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;
Expand Down Expand Up @@ -94,6 +97,25 @@ ResponseEntity<List<SingleBusTimeResponse>> getSearchTimetable(
@GetMapping("/courses")
ResponseEntity<List<BusCourseResponse>> getBusCourses();

@ApiResponses(
value = {
@ApiResponse(responseCode = "200"),
}
)
@Operation(summary = "학교버스 노선 조회")
@GetMapping("/courses/shuttle")
ResponseEntity<ShuttleBusRoutesResponse> getShuttleBusRoutes();

@ApiResponses(
value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
}
)
@Operation(summary = "학교버스 특정 노선 시간표 조회", description = "id: 노선 id 값 (get /bus/courses/shuttle response 참조)")
@GetMapping("/timetable/shuttle/{id}")
ResponseEntity<ShuttleBusTimetableResponse> getShuttleBusTimetable(@PathVariable String id);

@ApiResponses(
value = {
@ApiResponse(responseCode = "200"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -17,13 +18,16 @@
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.ShuttleBusRoutesResponse;
import in.koreatech.koin.domain.bus.dto.ShuttleBusTimetableResponse;
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;
import in.koreatech.koin.domain.bus.service.BusService;
import in.koreatech.koin.domain.bus.service.ShuttleBusService;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -32,6 +36,7 @@
public class BusController implements BusApi {

private final BusService busService;
private final ShuttleBusService shuttleBusService;

@GetMapping
public ResponseEntity<BusRemainTimeResponse> getBusRemainTime(
Expand Down Expand Up @@ -86,6 +91,16 @@ public ResponseEntity<List<SingleBusTimeResponse>> getSearchTimetable(
return ResponseEntity.ok().body(singleBusTimeResponses);
}

@GetMapping("/courses/shuttle")
public ResponseEntity<ShuttleBusRoutesResponse> getShuttleBusRoutes() {
return ResponseEntity.ok().body(shuttleBusService.getShuttleBusRoutes());
}

@GetMapping("/timetable/shuttle/{id}")
public ResponseEntity<ShuttleBusTimetableResponse> getShuttleBusTimetable(@PathVariable String id) {
return ResponseEntity.ok().body(shuttleBusService.getShuttleBusTimetable(id));
}

@GetMapping("/route")
public ResponseEntity<BusScheduleResponse> getBusRouteSchedule(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package in.koreatech.koin.domain.bus.dto;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

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

import in.koreatech.koin.domain.bus.model.enums.ShuttleBusRegion;
import in.koreatech.koin.domain.bus.model.enums.ShuttleRouteType;
import in.koreatech.koin.domain.bus.model.mongo.ShuttleBusRoute;
import in.koreatech.koin.domain.version.dto.VersionMessageResponse;
import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "셔틀버스 경로 응답")
public record ShuttleBusRoutesResponse(
@Schema(description = "노선 지역 분류 목록") List<RouteRegion> routeRegions,
@Schema(description = "학기 정보") RouteSemester semesterInfo
) {

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "노선 지역 정보")
public record RouteRegion(
@Schema(description = "지역 이름", example = "천안")
String region,

@Schema(description = "해당 지역의 경로 목록")
List<RouteName> routes
) {
}

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "노선 세부 정보")
public record RouteName(
@Schema(description = "노선 ID", example = "675013f9465776d6265ddfdb") String id,
@Schema(description = "노선 종류", example = "주말") String type,
@Schema(description = "노선 이름", example = "대학원") String routeName,
@Schema(description = "노선 부가 이름", example = "토요일") String subName
) {
}

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "학기 정보")
public record RouteSemester(
@Schema(description = "학기 이름", example = "정규학기") String name,
@Schema(description = "학기 시작 날짜", example = "2024-09-02") String from,
@Schema(description = "학기 종료 날짜", example = "2024-12-20") String to
) {
}

public static ShuttleBusRoutesResponse of(List<ShuttleBusRoute> shuttleBusRoutes,
VersionMessageResponse versionMessageResponse) {
List<RouteRegion> categories = mapCategories(shuttleBusRoutes);
String[] term = versionMessageResponse.content().split("~");
RouteSemester routeSemester = new RouteSemester(versionMessageResponse.title(), term[0].trim(), term[1].trim());
return new ShuttleBusRoutesResponse(categories, routeSemester);
}

private static List<RouteRegion> mapCategories(List<ShuttleBusRoute> shuttleBusRoutes) {
return shuttleBusRoutes.stream()
.collect(Collectors.groupingBy(ShuttleBusRoute::getRegion))
.entrySet().stream()
.map(entry -> new RouteRegion(entry.getKey().getLabel(), mapRouteNames(entry.getValue())))
.sorted(Comparator.comparingInt(o -> ShuttleBusRegion.getOrdinalByLabel(o.region())))
.toList();
}

private static List<RouteName> mapRouteNames(List<ShuttleBusRoute> routes) {
return routes.stream()
.map(route -> new RouteName(route.getId(), route.getRouteType().getLabel(), route.getRouteName(),
route.getSubName()))
.sorted(Comparator.comparingInt(o -> ShuttleRouteType.getOrdinalByLabel(o.type())))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package in.koreatech.koin.domain.bus.dto;

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.mongo.ShuttleBusRoute;
import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "셔틀버스 노선 응답")
public record ShuttleBusTimetableResponse(
@Schema(description = "노선 ID", example = "675013f9465776d6265ddfdb")
String id,

@Schema(description = "지역 이름", example = "천안")
String region,

@Schema(description = "노선 타입", example = "순환")
String routeType,

@Schema(description = "노선 이름", example = "천안 셔틀")
String routeName,

@Schema(description = "노선 부가 이름", example = "null")
String subName,

@Schema(description = "정류장 정보 목록")
List<NodeInfoResponse> nodeInfo,

@Schema(description = "회차 정보 목록")
List<RouteInfoResponse> routeInfo
) {

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "정류장 정보")
public record NodeInfoResponse(
@Schema(description = "정류장 이름", example = "캠퍼스 정문")
String name,

@Schema(description = "정류장 세부 정보", example = "정문 앞 정류장")
String detail
) {
}

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "노선 정보")
public record RouteInfoResponse(
@Schema(description = "노선 이름", example = "1회")
String name,

@Schema(description = "노선 세부 정보", example = "등교")
String detail,

@Schema(description = "도착 시간 목록", example = "[\"08:00\", \"09:00\"]")
List<String> arrivalTime
) {
}

public static ShuttleBusTimetableResponse from(ShuttleBusRoute shuttleBusRoute) {
List<NodeInfoResponse> nodeInfoResponses = shuttleBusRoute.getNodeInfo().stream()
.map(node -> new NodeInfoResponse(node.getName(), node.getDetail()))
.toList();
List<RouteInfoResponse> routeInfoResponses = shuttleBusRoute.getRouteInfo().stream()
.map(route -> new RouteInfoResponse(route.getName(), route.getDetail(), route.getArrivalTime()))
.toList();
return new ShuttleBusTimetableResponse(
shuttleBusRoute.getId(),
shuttleBusRoute.getRegion().getLabel(),
shuttleBusRoute.getRouteType().getLabel(),
shuttleBusRoute.getRouteName(),
shuttleBusRoute.getSubName(),
nodeInfoResponses,
routeInfoResponses
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package in.koreatech.koin.domain.bus.exception;

import in.koreatech.koin.global.exception.KoinIllegalArgumentException;

public class BusIllegalRegionException extends KoinIllegalArgumentException {

private static final String DEFAULT_MESSAGE = "버스 지역 구분이 잘못되었습니다.";

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

public BusIllegalRegionException(String message, String detail) {
super(message, detail);
}

public static BusIllegalStationException withDetail(String detail) {
return new BusIllegalStationException(DEFAULT_MESSAGE, detail);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package in.koreatech.koin.domain.bus.exception;

import in.koreatech.koin.global.exception.KoinIllegalArgumentException;

public class BusIllegalRouteTypeException extends KoinIllegalArgumentException {

private static final String DEFAULT_MESSAGE = "버스 노선 구분이 잘못되었습니다.";

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

public BusIllegalRouteTypeException(String message, String detail) {
super(message, detail);
}

public static BusIllegalStationException withDetail(String detail) {
return new BusIllegalStationException(DEFAULT_MESSAGE, detail);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package in.koreatech.koin.domain.bus.model.enums;

import in.koreatech.koin.domain.bus.exception.BusIllegalRegionException;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ShuttleBusRegion {
CHEONAN_ASAN("천안·아산"),
CHEONGJU("청주"),
SEOUL("서울"),
DAEJEON_SEJONG("대전·세종"),
;

private final String label;

public static int getOrdinalByLabel(String label) {
for (ShuttleBusRegion region : ShuttleBusRegion.values()) {
if (region.getLabel().equals(label)) {
return region.ordinal();
}
}
throw BusIllegalRegionException.withDetail("displayName: " + label);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package in.koreatech.koin.domain.bus.model.enums;

import in.koreatech.koin.domain.bus.exception.BusIllegalRouteTypeException;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ShuttleRouteType {
SHUTTLE("순환"),
WEEKDAYS("주중"),
WEEKEND("주말"),
;

private final String label;

public static int getOrdinalByLabel(String label) {
for (ShuttleRouteType shuttleRouteType : ShuttleRouteType.values()) {
if (shuttleRouteType.getLabel().equals(label)) {
return shuttleRouteType.ordinal();
}
}
throw BusIllegalRouteTypeException.withDetail("displayName: " + label);
}
}
Loading

0 comments on commit 91f7692

Please sign in to comment.