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 #1119

Merged
merged 13 commits into from
Dec 12, 2024
21 changes: 21 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 @@ -12,6 +12,8 @@
import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse;
import in.koreatech.koin.domain.bus.dto.BusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.CityBusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.ShuttleBusRouteResponse;
import in.koreatech.koin.domain.bus.dto.ShuttleBusRoutesResponse;
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;
Expand Down Expand Up @@ -90,4 +92,23 @@ ResponseEntity<List<SingleBusTimeResponse>> getSearchTimetable(
@Operation(summary = "버스 노선 조회")
@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 = "학교버스 시간표 조회")
@GetMapping("/timetable/shuttle")
ResponseEntity<ShuttleBusRouteResponse> getShuttleBusTimetable(@RequestParam String id);
Copy link
Contributor

Choose a reason for hiding this comment

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

id에 무슨 내용이 들어와야 하는지 @Opreation(description = "") 로 적어주시면 api 문서 안봐도 되서 편할 것 같아요

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse;
import in.koreatech.koin.domain.bus.dto.BusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.CityBusTimetableResponse;
import in.koreatech.koin.domain.bus.dto.ShuttleBusRouteResponse;
import in.koreatech.koin.domain.bus.dto.ShuttleBusRoutesResponse;
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 in.koreatech.koin.domain.bus.model.enums.CityBusDirection;
import in.koreatech.koin.domain.bus.repository.ShuttleBusRepository;
import in.koreatech.koin.domain.bus.service.BusService;
import in.koreatech.koin.domain.version.dto.VersionMessageResponse;
import in.koreatech.koin.domain.version.service.VersionService;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -29,6 +34,8 @@
public class BusController implements BusApi {

private final BusService busService;
private final ShuttleBusRepository shuttleBusRepository;
Copy link
Contributor

Choose a reason for hiding this comment

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

db로부터 받은 데이터를 단순히 response dto에 매핑시키는 간단한 로직이라 service 단 거치지 않는 구조로 만드신 것 같습니다. 이 과정에서 contoller에 repository 의존성이 추가되었는데 확장성이나 유지보수 고려하면 간단하더라도 service단 한번 거치는게 좋지 않을까요?

private final VersionService versionService;

@GetMapping
public ResponseEntity<BusRemainTimeResponse> getBusRemainTime(
Expand Down Expand Up @@ -82,4 +89,16 @@ public ResponseEntity<List<SingleBusTimeResponse>> getSearchTimetable(
depart, arrival);
return ResponseEntity.ok().body(singleBusTimeResponses);
}

@GetMapping("/courses/shuttle")
public ResponseEntity<ShuttleBusRoutesResponse> getShuttleBusRoutes() {
VersionMessageResponse version = versionService.getVersionWithMessage("shuttle_bus_timetable");
return ResponseEntity.ok()
.body(ShuttleBusRoutesResponse.of(shuttleBusRepository.findBySemesterType(version.title()), version));
}

@GetMapping("/timetable/shuttle")
public ResponseEntity<ShuttleBusRouteResponse> getShuttleBusTimetable(@RequestParam String id) {
return ResponseEntity.ok().body(ShuttleBusRouteResponse.from(shuttleBusRepository.getById(id)));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

A

controller에서 repository에 직접 접근하고 있군요..!
service를 반드시 껴야하는 건 아니긴 한데, 괜찮은 구조라고 생각하시는지 궁금해요
저도 잘 확신이 안서네요..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

controller 입장에서 계층적으로 service는 가까운 친구이지만, repository는 멀리있는 친구이기 때문에 service만 알고 있는 것도 좋은 방법 같습니다. (repository에 직접 참조한다면 당장은 편하겠지만..)
우선 service에 의존하도록 하고 bus 리팩토링 기간 조금 더 고민해보도록 하겠습니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 = "셔틀버스 경로 응답 DTO")
public record ShuttleBusRouteResponse(
@Schema(description = "경로 ID", example = "675013f9465776d6265ddfdb")
String id,

@Schema(description = "지역 이름", example = "천안")
String region,
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을 적용하는 건 어떻게 생각하시나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

enum 사용 시, 정렬할 때 우선순위 값으로 활용할 수 있고 확장성이 향상되는 등 객체지향 기법에 적절하다고 생각해서 말씀하신대로 수정해보겠습니다👍


@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 = "노드 정보")
private record NodeInfoResponse(
@Schema(description = "노드 이름", example = "캠퍼스 정문")
String name,

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

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "경로 정보")
private record RouteInfoResponse(
@Schema(description = "경로 이름", example = "1회")
String name,

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

public static ShuttleBusRouteResponse 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.getArrivalTime()))
.toList();
return new ShuttleBusRouteResponse(
shuttleBusRoute.getId(),
shuttleBusRoute.getRegion(),
shuttleBusRoute.getRouteType(),
shuttleBusRoute.getRouteName(),
shuttleBusRoute.getSubName(),
nodeInfoResponses,
routeInfoResponses
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package in.koreatech.koin.domain.bus.dto;

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

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

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

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 = "노선 지역 정보")
private record RouteRegion(
@Schema(description = "지역 이름", example = "천안") String region,
@Schema(description = "해당 지역의 경로 목록") List<RouteName> routes
) implements Comparable<RouteRegion> {

@Override
public int compareTo(RouteRegion routeName) {
return Integer.compare(getPriority(this.region), getPriority(routeName.region));
}

private int getPriority(String region) {
return switch (region) {
case "천안" -> 1;
case "청주" -> 2;
case "서울" -> 3;
default -> 4;
};
}

public static List<RouteRegion> mapCategories(List<ShuttleBusRoute> shuttleBusRoutes) {
return shuttleBusRoutes.stream()
.collect(Collectors.groupingBy(ShuttleBusRoute::getRegion))
.entrySet().stream()
.map(entry -> new RouteRegion(entry.getKey(), RouteName.mapRouteNames(entry.getValue())))
.sorted()
.toList();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

데이터 전송 객체가 너무 많은 책임을 가지고 있는 것 같아요. service 사용하지 않은 이유가 궁금해요.

Copy link
Contributor Author

@kih1015 kih1015 Dec 11, 2024

Choose a reason for hiding this comment

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

Service에서 특정 DTO에 의존하게 되면 여러 종류의 Controller에서 해당 Service를 이용할 수 없어 코드 재사용성이 떨어지게 된다라고 생각해서 변환의 책임을 DTO에 두었습니다.

그런데 다시 생각해보니 말씀하신대로 데이터 전송 객체에 무거운 책임을 두는 것은 좋지 않은 생각인 것 같아요. service 사용하도록 수정하겠습니다. 👍

참조


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

@Override
public int compareTo(RouteName routeName) {
return Integer.compare(getPriority(this.type), getPriority(routeName.type));
}

private int getPriority(String type) {
return switch (type) {
case "순환" -> 1;
case "주중" -> 2;
case "주말" -> 3;
default -> 4;
};
}

public static List<RouteName> mapRouteNames(List<ShuttleBusRoute> routes) {
return routes.stream()
.map(route -> new RouteName(route.getId(), route.getRouteType(), route.getRouteName(), route.getSubName()))
.sorted()
.toList();
}
}

@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "학기 정보")
private record RouteSemester(
@Schema(description = "학기 이름", example = "정규학기") String name,
@Schema(description = "학기 기간", example = "2024-09-02 ~ 2024-12-20") String term
) {

public static RouteSemester from(VersionMessageResponse versionMessageResponse) {
return new RouteSemester(versionMessageResponse.title(),
versionMessageResponse.content());
}
}

public static ShuttleBusRoutesResponse of(List<ShuttleBusRoute> shuttleBusRoutes,
VersionMessageResponse versionMessageResponse) {
List<RouteRegion> categories = RouteRegion.mapCategories(shuttleBusRoutes);
RouteSemester routeSemester = RouteSemester.from(versionMessageResponse);
return new ShuttleBusRoutesResponse(categories, routeSemester);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package in.koreatech.koin.domain.bus.model.mongo;

import static lombok.AccessLevel.PROTECTED;

import java.util.List;

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

import com.fasterxml.jackson.annotation.JsonIgnore;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = PROTECTED)
@Document(collection = "shuttlebus_timetables")
public class ShuttleBusRoute {

@Id
private String id;

@Field("semester_type")
private String semesterType;

@Field("region")
private String region;

@Field("route_type")
private String routeType;

@Field("route_name")
private String routeName;

@Field("sub_name")
private String subName;

@Field("node_info")
private List<NodeInfo> nodeInfo;

@Field("route_info")
private List<RouteInfo> routeInfo;

@Getter
@NoArgsConstructor(access = PROTECTED)
public static class NodeInfo {

@Field("name")
private String name;

@Field("detail")
private String detail;
}

@Getter
@NoArgsConstructor(access = PROTECTED)
public static class RouteInfo {

@Field("name")
private String name;

@Field("running_days")
@JsonIgnore
private List<String> runningDays;

@Field("arrival_time")
private List<String> arrivalTime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package in.koreatech.koin.domain.bus.repository;

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

import org.bson.types.ObjectId;
import org.springframework.data.repository.Repository;

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

public interface ShuttleBusRepository extends Repository<ShuttleBusRoute, ObjectId> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

A

ObjectId는 무슨 자료형인가요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ObjectId: mongodb 방식에 특화되어 기본적으로 제공하는 id 타입으로 알고있습니다.
spring data mongodb에서 자바와 맵핑 가능하도록 제공하는 것으로 보입니다.
jpa에서 string이 아닌, 해당 타입으로 설정해야 id로 검색이 가능해서 설정해두었습니다.


List<ShuttleBusRoute> findBySemesterType(String semesterType);

Optional<ShuttleBusRoute> findById(String id);

default ShuttleBusRoute getById(String id) {
return findById(id).orElseThrow(
() -> BusNotFoundException.withDetail("id: " + id));
}
}
Loading