diff --git a/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java b/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java index 0271ab668..0811fe167 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java +++ b/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java @@ -10,6 +10,7 @@ import in.koreatech.koin.domain.bus.dto.BusCourseResponse; import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse; +import in.koreatech.koin.domain.bus.dto.BusTimetableResponse; 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; @@ -48,6 +49,14 @@ ResponseEntity> getBusTimetable( @RequestParam(value = "region") String region ); + @Operation(summary = "버스 시간표 조회") + @GetMapping("/timetable/v2") + ResponseEntity getBusTimetableV2( + @RequestParam(value = "bus_type") BusType busType, + @RequestParam(value = "direction") String direction, + @RequestParam(value = "region") String region + ); + @ApiResponses( value = { @ApiResponse(responseCode = "200"), diff --git a/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java b/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java index 51f59ff0d..231d85da4 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java +++ b/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java @@ -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.dto.BusTimetableResponse; 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; @@ -46,6 +47,15 @@ public ResponseEntity> getBusTimetable( return ResponseEntity.ok().body(busService.getBusTimetable(busType, direction, region)); } + @GetMapping("/timetable/v2") + public ResponseEntity getBusTimetableV2( + @RequestParam(value = "bus_type") BusType busType, + @RequestParam(value = "direction") String direction, + @RequestParam(value = "region") String region + ){ + return ResponseEntity.ok().body(busService.getBusTimetableWithUpdatedAt(busType, direction, region)); + } + @GetMapping("/courses") public ResponseEntity> getBusCourses() { return ResponseEntity.ok().body(busService.getBusCourses()); diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusTimetableResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusTimetableResponse.java new file mode 100644 index 000000000..3505aa276 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusTimetableResponse.java @@ -0,0 +1,30 @@ +package in.koreatech.koin.domain.bus.dto; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; + +import java.time.LocalDateTime; +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.bus.model.BusTimetable; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(SnakeCaseStrategy.class) +public record BusTimetableResponse( + @Schema(description = "버스 시간표", example = """ + { + "route_name": "주말(14시 35분)", + "arrival_info": { + "nodeName": "터미널(신세계 앞 횡단보도)", + "arrivalTime": "14:35" + } + } + """, requiredMode = NOT_REQUIRED) + List busTimetable, + + @Schema(description = "업데이트 시각", example = "2024-04-20 18:00:00", requiredMode = NOT_REQUIRED) + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java b/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java index 881429205..8fce726cc 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java @@ -16,6 +16,7 @@ import in.koreatech.koin.domain.bus.dto.BusCourseResponse; import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse; +import in.koreatech.koin.domain.bus.dto.BusTimetableResponse; import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse; import in.koreatech.koin.domain.bus.exception.BusIllegalStationException; import in.koreatech.koin.domain.bus.exception.BusTypeNotFoundException; @@ -31,6 +32,8 @@ import in.koreatech.koin.domain.bus.repository.BusRepository; import in.koreatech.koin.domain.bus.util.CityBusOpenApiClient; import in.koreatech.koin.domain.bus.util.ExpressBusOpenApiClient; +import in.koreatech.koin.domain.version.dto.VersionResponse; +import in.koreatech.koin.domain.version.service.VersionService; import lombok.RequiredArgsConstructor; @Service @@ -42,6 +45,7 @@ public class BusService { private final BusRepository busRepository; private final CityBusOpenApiClient cityBusOpenApiClient; private final ExpressBusOpenApiClient expressBusOpenApiClient; + private final VersionService versionService; @Transactional public BusRemainTimeResponse getBusRemainTime(BusType busType, BusStation depart, BusStation arrival) { @@ -178,6 +182,17 @@ public List getBusTimetable(BusType busType, String dire throw new BusTypeNotFoundException(busType.name()); } + public BusTimetableResponse getBusTimetableWithUpdatedAt(BusType busType, String direction, String region){ + List BusTimetables = getBusTimetable(busType, direction, region); + + if (busType.equals(BusType.COMMUTING)){ + busType = BusType.SHUTTLE; + } + + VersionResponse version = versionService.getVersion(busType.name().toLowerCase() + "_bus_timetable"); + return new BusTimetableResponse(BusTimetables, version.updatedAt()); + } + public List getBusCourses() { return busRepository.findAll().stream() .map(BusCourseResponse::from) diff --git a/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java b/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java index a8dddd92a..266317100 100644 --- a/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java @@ -40,6 +40,7 @@ import in.koreatech.koin.domain.bus.repository.ExpressBusCacheRepository; import in.koreatech.koin.domain.version.model.Version; import in.koreatech.koin.domain.version.repository.VersionRepository; +import in.koreatech.koin.domain.version.service.VersionService; import in.koreatech.koin.support.JsonAssertions; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; @@ -59,6 +60,9 @@ class BusApiTest extends AcceptanceTest { @Autowired private ExpressBusCacheRepository expressBusCacheRepository; + @Autowired + private VersionService versionService; + private final Instant UPDATED_AT = ZonedDateTime.parse( "2024-02-21 18:00:00 KST", ofPattern("yyyy-MM-dd " + "HH:mm:ss z") @@ -137,11 +141,11 @@ void getNextShuttleBusRemainTime() { softly -> { softly.assertThat(response.body().jsonPath().getString("bus_type")) .isEqualTo(busType.name().toLowerCase()); - softly.assertThat((Long) response.body().jsonPath().get("now_bus.bus_number")).isNull(); + softly.assertThat((Long)response.body().jsonPath().get("now_bus.bus_number")).isNull(); softly.assertThat(response.body().jsonPath().getLong("now_bus.remain_time")).isEqualTo( BusRemainTime.from(arrivalTime).getRemainSeconds(clock)); - softly.assertThat((Long) response.body().jsonPath().get("next_bus.bus_number")).isNull(); - softly.assertThat((Long) response.body().jsonPath().get("next_bus.remain_time")).isNull(); + softly.assertThat((Long)response.body().jsonPath().get("next_bus.bus_number")).isNull(); + softly.assertThat((Long)response.body().jsonPath().get("next_bus.remain_time")).isNull(); } ); } @@ -200,8 +204,8 @@ void getNextCityBusRemainTimeRedis() { softly -> { softly.assertThat(response.body().jsonPath().getString("bus_type")) .isEqualTo(busType.name().toLowerCase()); - softly.assertThat((Long) response.body().jsonPath().getLong("now_bus.bus_number")).isEqualTo(busNumber); - softly.assertThat((Long) response.body().jsonPath().getLong("now_bus.remain_time")) + softly.assertThat((Long)response.body().jsonPath().getLong("now_bus.bus_number")).isEqualTo(busNumber); + softly.assertThat((Long)response.body().jsonPath().getLong("now_bus.remain_time")) .isEqualTo( BusRemainTime.of(remainTime, version.getUpdatedAt().toLocalTime()).getRemainSeconds(clock)); softly.assertThat(response.body().jsonPath().getObject("next_bus.bus_number", Long.class)).isNull(); @@ -303,12 +307,12 @@ void getNextCityBusRemainTimeOpenApi() { softly -> { softly.assertThat(response.body().jsonPath().getString("bus_type")) .isEqualTo(busType.name().toLowerCase()); - softly.assertThat((Long) response.body().jsonPath().getLong("now_bus.bus_number")).isEqualTo(400); - softly.assertThat((Long) response.body().jsonPath().getLong("now_bus.remain_time")) + softly.assertThat((Long)response.body().jsonPath().getLong("now_bus.bus_number")).isEqualTo(400); + softly.assertThat((Long)response.body().jsonPath().getLong("now_bus.remain_time")) .isEqualTo( BusRemainTime.of(600L, version.getUpdatedAt().toLocalTime()).getRemainSeconds(clock)); - softly.assertThat((Long) response.body().jsonPath().getLong("next_bus.bus_number")).isEqualTo(405); - softly.assertThat((Long) response.body().jsonPath().getLong("next_bus.remain_time")) + softly.assertThat((Long)response.body().jsonPath().getLong("next_bus.bus_number")).isEqualTo(405); + softly.assertThat((Long)response.body().jsonPath().getLong("next_bus.remain_time")) .isEqualTo( BusRemainTime.of(800L, version.getUpdatedAt().toLocalTime()).getRemainSeconds(clock)); } @@ -470,4 +474,60 @@ void getShuttleBusTimetable() { ] """); } + + @Test + @DisplayName("셔틀버스 시간표를 조회한다(업데이트 시각 포함).") + void getShuttleBusTimetableWithUpdatedAt() { + when(dateTimeProvider.getNow()).thenReturn(Optional.of(UPDATED_AT)); + + Version version = Version.builder() + .version("20240_1712920946") + .type("shuttle_bus_timetable") + .build(); + versionRepository.save(version); + + BusType busType = BusType.from("shuttle"); + String direction = "from"; + String region = "천안"; + + ExtractableResponse response = RestAssured + .given() + .when() + .param("bus_type", busType.name().toLowerCase()) + .param("direction", direction) + .param("region", region) + .get("/bus/timetable/v2") + .then() + .statusCode(HttpStatus.OK.value()) + .extract(); + + JsonAssertions.assertThat(response.asPrettyString()) + .isEqualTo(String.format(""" + { + "bus_timetable": [ + { + "route_name": "주중", + "arrival_info": [ + { + "nodeName": "한기대", + "arrivalTime": "18:10" + }, + { + "nodeName": "신계초,운전리,연춘리", + "arrivalTime": "정차" + }, + { + "nodeName": "천안역(학화호두과자)", + "arrivalTime": "18:50" + },{ + "nodeName": "터미널(신세계 앞 횡단보도)", + "arrivalTime": "18:55" + } + ] + } + ], + "updated_at": %s + } + """, version.getUpdatedAt())); + } }