From f9c9d8d4f8890364232495644cae79d711f6b5f3 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 14:45:13 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat=20:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EB=B9=84=EC=8A=A4=EB=8B=88=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bus/model/mongo/CityBusTimetable.java | 36 ++++++++++++ .../koin/domain/bus/model/mongo/Route.java | 56 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java index 7ca75c899..11ceeb9b6 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java @@ -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.model.enums.BusStation; +import in.koreatech.koin.domain.bus.model.enums.CityBusDirection; import jakarta.persistence.Id; import lombok.AccessLevel; import lombok.Builder; @@ -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; + @Id @Field("_id") private String routeId; @@ -38,6 +49,13 @@ private CityBusTimetable(BusInfo busInfo, List busTimetables, Loca this.updatedAt = updatedAt; } + public void filterBusTimeTablesByDayOfWeek(LocalDate date) { + String dayOfWeek = (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) ? + "주말" : "평일"; + + busTimetables.removeIf(busTimetable -> !busTimetable.getDayOfWeek().equals(dayOfWeek)); + } + @Getter public static class BusInfo { @@ -71,5 +89,23 @@ private BusTimetable(String dayOfWeek, List departInfo) { this.dayOfWeek = dayOfWeek; this.departInfo = departInfo; } + + public List applyTimeOffset(Long busNumber, CityBusDirection arrival, BusStation depart) { + return departInfo.stream() + .map(time -> { + LocalTime schedule = LocalTime.parse(time); + if (busNumber == 400 && arrival == CityBusDirection.종합터미널) { + schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_KOREATECH_400); + } else if (busNumber == 402 && arrival == CityBusDirection.종합터미널) { + schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_KOREATECH_402); + } else if (busNumber == 405 && arrival == CityBusDirection.종합터미널) { + 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()); + } } } diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java index 48aab2069..2887487fa 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java @@ -1,7 +1,10 @@ package in.koreatech.koin.domain.bus.model.mongo; import java.time.Clock; +import java.time.DayOfWeek; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.TextStyle; import java.util.ArrayList; import java.util.List; @@ -9,6 +12,7 @@ import org.springframework.data.mongodb.core.mapping.Field; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; import in.koreatech.koin.domain.bus.exception.BusArrivalNodeNotFoundException; import in.koreatech.koin.domain.bus.model.BusRemainTime; import in.koreatech.koin.domain.bus.model.enums.BusStation; @@ -68,6 +72,58 @@ private ArrivalNode convertToArrivalNode(BusStation busStation) { "routeName: " + routeName + ", busStation: " + busStation.name())); } + public boolean filterRoutesByDayOfWeek(LocalDate date) { + DayOfWeek dayOfWeek = date.getDayOfWeek(); + return runningDays.contains(dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase()); + } + + public boolean filterCircularRoutes() { + if (arrivalInfos.isEmpty()) { + return false; + } + + String firstNodeName = arrivalInfos.get(0).getNodeName(); + String lastNodeName = arrivalInfos.get(arrivalInfos.size() - 1).getNodeName(); + + return firstNodeName.equals(lastNodeName); + } + + public boolean filterDepartAndArriveNode(BusStation departNode, BusStation arriveNode) { + boolean foundDepart = false; + + for (ArrivalNode node : arrivalInfos) { + if (!foundDepart && node.getNodeName().contains(departNode.getQueryName())) { + foundDepart = true; + } + + else if (foundDepart && node.getNodeName().contains(arriveNode.getQueryName())) { + return true; + } + } + + return false; + } + + public ScheduleInfo getShuttleBusScheduleInfo(BusStation depart) { + ArrivalNode findDepartNode = findArrivalNodeByStation(depart); + return new ScheduleInfo("shuttle", routeName, LocalTime.parse(findDepartNode.getArrivalTime())); + } + + public ScheduleInfo getCommutingShuttleBusScheduleInfo(BusStation depart) { + String busType = "한기대".equals(depart.getQueryName()) ? "하교셔틀" : "등교셔틀"; + ArrivalNode findDepartNode = findArrivalNodeByStation(depart); + + return new ScheduleInfo("shuttle", String.format("%s, %s", routeName, busType), + LocalTime.parse(findDepartNode.getArrivalTime())); + } + + private ArrivalNode findArrivalNodeByStation(BusStation depart) { + return arrivalInfos.stream() + .filter(arrivalNode -> arrivalNode.getNodeName().contains(depart.getQueryName())) + .findFirst() + .orElseThrow(() -> new BusArrivalNodeNotFoundException("")); + } + @Builder private Route(String routeName, List runningDays, List arrivalInfos) { this.routeName = routeName; From f62e32286ea57c9f84b83eb87201ddaa1cfcf12e Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 14:45:32 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat=20:=20command=20DTO=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/dto/BusRouteCommand.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java new file mode 100644 index 000000000..75e31a390 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java @@ -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; + } +} From c138c80a381ff71e8137fd3f3f22ae72701febde Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 14:46:10 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat=20:=20BusStation=20enum=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/model/enums/BusStation.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/enums/BusStation.java b/src/main/java/in/koreatech/koin/domain/bus/model/enums/BusStation.java index 684b286f0..ab2ca6270 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/enums/BusStation.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/enums/BusStation.java @@ -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 displayNames; private final BusStationNode node; + private final String queryName; - BusStation(List displayNames, BusStationNode node) { + BusStation(List displayNames, BusStationNode node, String queryName) { this.displayNames = displayNames; this.node = node; + this.queryName = queryName; } @JsonCreator From 70e5b386082c4b23f033b7e4c353f03c437b09f5 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 14:47:16 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat=20:=20repository=20query=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/koreatech/koin/domain/bus/repository/BusRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/in/koreatech/koin/domain/bus/repository/BusRepository.java b/src/main/java/in/koreatech/koin/domain/bus/repository/BusRepository.java index f6a6ca3ff..2d6505830 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/repository/BusRepository.java +++ b/src/main/java/in/koreatech/koin/domain/bus/repository/BusRepository.java @@ -16,6 +16,8 @@ public interface BusRepository extends Repository { List findByBusType(String busType); + List findByBusTypeAndRegion(String busType, String region); + Optional findByBusTypeAndDirectionAndRegion(String busType, String direction, String region); default BusCourse getByBusTypeAndDirectionAndRegion(String busType, String direction, String region) { From 180f899d37c559d641b4a5a9d04bcf5155802050 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 14:47:58 +0900 Subject: [PATCH 05/27] =?UTF-8?q?feat=20:=20=EC=A0=84=EB=9E=B5=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/bus/service/route/BusRouteStrategy.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java new file mode 100644 index 000000000..162424287 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java @@ -0,0 +1,12 @@ +package in.koreatech.koin.domain.bus.service.route; + +import java.util.List; + +import in.koreatech.koin.domain.bus.dto.BusRouteCommand; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.model.enums.BusRouteType; + +public interface BusRouteStrategy { + List findSchedule(BusRouteCommand command); + boolean support(BusRouteType type); +} From 8f487af4fe9a4160baee8dad3ffa392c41aa77be Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 14:48:30 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat=20:=20=EC=A0=84=EB=9E=B5=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route/CircularShuttleBusStrategy.java | 40 +++++++++++ .../service/route/CityBusRouteStrategy.java | 53 ++++++++++++++ .../CommutingShuttleBusRouteStrategy.java | 39 +++++++++++ .../route/ExpressBusRouteStrategy.java | 69 +++++++++++++++++++ .../route/NormalShuttleBusStrategy.java | 35 ++++++++++ 5 files changed, 236 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/route/CircularShuttleBusStrategy.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingShuttleBusRouteStrategy.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CircularShuttleBusStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/CircularShuttleBusStrategy.java new file mode 100644 index 000000000..d1dc778ad --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/CircularShuttleBusStrategy.java @@ -0,0 +1,40 @@ +package in.koreatech.koin.domain.bus.service.route; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import in.koreatech.koin.domain.bus.dto.BusRouteCommand; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.model.enums.BusRouteType; +import in.koreatech.koin.domain.bus.model.mongo.BusCourse; +import in.koreatech.koin.domain.bus.model.mongo.Route; +import in.koreatech.koin.domain.bus.repository.BusRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CircularShuttleBusStrategy implements BusRouteStrategy{ + + private final BusRepository busRepository; + + @Override + public List findSchedule(BusRouteCommand command) { + return busRepository.findByBusTypeAndRegion("shuttle", "천안").stream() + .map(BusCourse::getRoutes) + .flatMap(routes -> + routes.stream() + .filter(route -> route.filterRoutesByDayOfWeek(command.date())) + .filter(Route::filterCircularRoutes) + .filter(route -> route.filterDepartAndArriveNode(command.depart(), command.arrive())) + .map(route -> route.getShuttleBusScheduleInfo(command.depart())) + ) + .distinct() + .toList(); + } + + @Override + public boolean support(BusRouteType type) { + return type == BusRouteType.SHUTTLE || type == BusRouteType.ALL; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java new file mode 100644 index 000000000..8f854ddfa --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java @@ -0,0 +1,53 @@ +package in.koreatech.koin.domain.bus.service.route; + +import static in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo.toScheduleInfo; + +import java.time.LocalTime; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.stereotype.Service; + +import in.koreatech.koin.domain.bus.dto.BusRouteCommand; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.model.city.CityBusRouteType; +import in.koreatech.koin.domain.bus.model.enums.BusRouteType; +import in.koreatech.koin.domain.bus.model.mongo.CityBusTimetable; +import in.koreatech.koin.domain.bus.repository.CityBusTimetableRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CityBusRouteStrategy implements BusRouteStrategy{ + + private final CityBusTimetableRepository cityBusTimetableRepository; + + @Override + public List findSchedule(BusRouteCommand command) { + return CityBusRouteType.findRoutes(command.depart(), command.arrive()) + .stream() + .flatMap(route -> getScheduleForRoute(route, command)) + .toList(); + } + + private Stream getScheduleForRoute(CityBusRouteType route, BusRouteCommand command) { + CityBusTimetable cityBusSchedule = cityBusTimetableRepository + .getByBusInfoNumberAndBusInfoArrival(route.getBusNumber(), route.getArrivalStation().getName()); + + cityBusSchedule.filterBusTimeTablesByDayOfWeek(command.date()); + + // 시내버스 도착 시간 보정(기점 ~ 한기대 정류장 까지의 시간 추가) + List adjustedTimetable = cityBusSchedule.getBusTimetables() + .stream() + .findFirst() + .get() + .applyTimeOffset(route.getBusNumber(), route.getArrivalStation(), command.depart()); + + return toScheduleInfo(adjustedTimetable, "city", route.getBusNumber().toString()).stream(); + } + + @Override + public boolean support(BusRouteType type) { + return type == BusRouteType.CITY || type == BusRouteType.ALL; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingShuttleBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingShuttleBusRouteStrategy.java new file mode 100644 index 000000000..38b8128b2 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingShuttleBusRouteStrategy.java @@ -0,0 +1,39 @@ +package in.koreatech.koin.domain.bus.service.route; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import in.koreatech.koin.domain.bus.dto.BusRouteCommand; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.model.enums.BusRouteType; +import in.koreatech.koin.domain.bus.model.mongo.BusCourse; +import in.koreatech.koin.domain.bus.repository.BusRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CommutingShuttleBusRouteStrategy implements BusRouteStrategy{ + + private final BusRepository busRepository; + + @Override + public List findSchedule(BusRouteCommand command) { + return busRepository.findByBusTypeAndRegion("commuting", "천안").stream() + .map(BusCourse::getRoutes) + .flatMap(routes -> + routes.stream() + .filter(route -> route.filterRoutesByDayOfWeek(command.date())) + .filter(route -> route.filterDepartAndArriveNode(command.depart(), command.arrive())) + .map(route -> route.getCommutingShuttleBusScheduleInfo(command.depart())) + ) + .distinct() + .toList(); + } + + @Override + public boolean support(BusRouteType type) { + return type == BusRouteType.SHUTTLE || type == BusRouteType.ALL; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java new file mode 100644 index 000000000..5758e731c --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java @@ -0,0 +1,69 @@ +package in.koreatech.koin.domain.bus.service.route; + +import static in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo.toScheduleInfo; + +import java.time.LocalTime; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.stereotype.Component; + +import in.koreatech.koin.domain.bus.dto.BusRouteCommand; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.model.enums.BusDirection; +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.express.ExpressBusRouteSchedule; + +@Component +public class ExpressBusRouteStrategy implements BusRouteStrategy{ + + private static final Map> VALID_ROUTES = createValidRoutes(); + private static final String BUS_TYPE = "express"; + private static final String BUS_NAME = "대성티엔이"; + + @Override + public List findSchedule(BusRouteCommand command) { + if (!isValidRoute(command.depart(), command.arrive())) { + return Collections.emptyList(); + } + + BusDirection direction = getRouteDirection(command.depart(), command.arrive()); + List departTimes = getStaticExpressBusScheduleTimeList(direction); + return toScheduleInfo(departTimes, BUS_TYPE, BUS_NAME); + } + + @Override + public boolean support(BusRouteType type) { + return type == BusRouteType.EXPRESS || type == BusRouteType.ALL; + } + + private static List getStaticExpressBusScheduleTimeList(BusDirection direction) { + return switch (direction) { + case NORTH -> ExpressBusRouteSchedule.getExpressBusScheduleToKoreaTech(); + case SOUTH -> ExpressBusRouteSchedule.getExpressBusScheduleToTerminal(); + }; + } + + private BusDirection getRouteDirection(BusStation depart, BusStation arrive) { + return (depart == BusStation.KOREATECH && arrive == BusStation.TERMINAL) + ? BusDirection.NORTH : BusDirection.SOUTH; + } + + private boolean isValidRoute(BusStation depart, BusStation arrive) { + return VALID_ROUTES.getOrDefault(depart, Collections.emptySet()).contains(arrive); + } + + private static Map> createValidRoutes() { + Map> routes = new EnumMap<>(BusStation.class); + + routes.put(BusStation.KOREATECH, EnumSet.of(BusStation.TERMINAL)); + routes.put(BusStation.TERMINAL, EnumSet.of(BusStation.KOREATECH)); + + return Collections.unmodifiableMap(routes); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java new file mode 100644 index 000000000..7539e10f5 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java @@ -0,0 +1,35 @@ +package in.koreatech.koin.domain.bus.service.route; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import in.koreatech.koin.domain.bus.dto.BusRouteCommand; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.model.enums.BusRouteType; +import in.koreatech.koin.domain.bus.repository.BusRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class NormalShuttleBusStrategy implements BusRouteStrategy{ + + private final BusRepository busRepository; + + @Override + public List findSchedule(BusRouteCommand command) { + String direction = command.depart().getQueryName() == "한기대" ? "from" : "to"; + return busRepository.getByBusTypeAndDirectionAndRegion("shuttle", direction, "천안").getRoutes() + .stream() + .filter(route -> route.filterRoutesByDayOfWeek(command.date())) + .filter(route -> !route.filterCircularRoutes()) + .filter(route -> route.filterDepartAndArriveNode(command.depart(), command.arrive())) + .map(route -> route.getShuttleBusScheduleInfo(command.depart())) + .toList(); + } + + @Override + public boolean support(BusRouteType type) { + return type == BusRouteType.SHUTTLE || type == BusRouteType.ALL; + } +} From fa057135e01ad80b596f52fc5774e3719e85a5d2 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 14:49:36 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat=20:=20response=20DTO=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/bus/dto/BusScheduleResponse.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java new file mode 100644 index 000000000..11ec65194 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java @@ -0,0 +1,66 @@ +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.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 schedule + +) { + @JsonNaming(SnakeCaseStrategy.class) + public record ScheduleInfo( + String busType, + String busName, + LocalTime departTime + ) { + public static List toScheduleInfo(List timetable, String busType, String busName) { + return timetable.stream() + .map(time -> new BusScheduleResponse.ScheduleInfo(busType, busName, time)) + .collect(Collectors.toList()); + } + } +} From ab7e34480b8d898fa9370d942add2837c090303b Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 16:52:40 +0900 Subject: [PATCH 08/27] =?UTF-8?q?feat=20:=20=EC=8B=9C=EB=82=B4=EB=B2=84?= =?UTF-8?q?=EC=8A=A4=20=EC=8A=A4=EC=BC=80=EC=A4=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bus/model/mongo/CityBusTimetable.java | 30 +++++++++----- .../CityBusTimetableRepository.java | 11 ++++- .../service/route/CityBusRouteStrategy.java | 41 ++++++++----------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java index 11ceeb9b6..195bd1f8a 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java @@ -11,8 +11,8 @@ 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 in.koreatech.koin.domain.bus.model.enums.CityBusDirection; import jakarta.persistence.Id; import lombok.AccessLevel; import lombok.Builder; @@ -49,11 +49,13 @@ private CityBusTimetable(BusInfo busInfo, List busTimetables, Loca this.updatedAt = updatedAt; } - public void filterBusTimeTablesByDayOfWeek(LocalDate date) { - String dayOfWeek = (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) ? - "주말" : "평일"; - - busTimetables.removeIf(busTimetable -> !busTimetable.getDayOfWeek().equals(dayOfWeek)); + public List 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 @@ -90,15 +92,23 @@ private BusTimetable(String dayOfWeek, List departInfo) { this.departInfo = departInfo; } - public List applyTimeOffset(Long busNumber, CityBusDirection arrival, BusStation depart) { + 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 applyTimeOffset(Long busNumber, BusStation depart) { return departInfo.stream() .map(time -> { LocalTime schedule = LocalTime.parse(time); - if (busNumber == 400 && arrival == CityBusDirection.종합터미널) { + if (busNumber == 400 && depart == BusStation.KOREATECH) { schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_KOREATECH_400); - } else if (busNumber == 402 && arrival == CityBusDirection.종합터미널) { + } else if (busNumber == 402 && depart == BusStation.KOREATECH) { schedule = schedule.plusMinutes(ADDITIONAL_TIME_DEPART_TO_KOREATECH_402); - } else if (busNumber == 405 && arrival == CityBusDirection.종합터미널) { + } 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); diff --git a/src/main/java/in/koreatech/koin/domain/bus/repository/CityBusTimetableRepository.java b/src/main/java/in/koreatech/koin/domain/bus/repository/CityBusTimetableRepository.java index 252ca36a9..6d631a443 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/repository/CityBusTimetableRepository.java +++ b/src/main/java/in/koreatech/koin/domain/bus/repository/CityBusTimetableRepository.java @@ -15,6 +15,15 @@ public interface CityBusTimetableRepository extends Repository BusCacheNotFoundException.withDetail("number: " + number + ", direction: " + arrivalNode + " 기점 방향")); + .orElseThrow(() -> BusCacheNotFoundException.withDetail( + "number: " + number + ", direction: " + arrivalNode + " 기점 방향")); + } + + Optional findByBusInfoNumberAndBusInfoDepart(Long number, String departNode); + + default CityBusTimetable getByBusInfoNumberAndBusInfoDepart(Long number, String departNode) { + return findByBusInfoNumberAndBusInfoDepart(number, departNode) + .orElseThrow(() -> BusCacheNotFoundException.withDetail( + "number: " + number + ", direction: " + departNode + " 종점 방향")); } } diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java index 8f854ddfa..7c19695fe 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java @@ -1,49 +1,44 @@ package in.koreatech.koin.domain.bus.service.route; -import static in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo.toScheduleInfo; - -import java.time.LocalTime; import java.util.List; -import java.util.stream.Stream; +import java.util.Map; import org.springframework.stereotype.Service; import in.koreatech.koin.domain.bus.dto.BusRouteCommand; import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; -import in.koreatech.koin.domain.bus.model.city.CityBusRouteType; 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.CityBusDirection; import in.koreatech.koin.domain.bus.model.mongo.CityBusTimetable; import in.koreatech.koin.domain.bus.repository.CityBusTimetableRepository; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor -public class CityBusRouteStrategy implements BusRouteStrategy{ +public class CityBusRouteStrategy implements BusRouteStrategy { private final CityBusTimetableRepository cityBusTimetableRepository; + private static final Map CITY_BUS_INFO = Map.of( + 400L, CityBusDirection.병천3리, + 402L, CityBusDirection.황사동, + 405L, CityBusDirection.유관순열사사적지 + ); @Override public List findSchedule(BusRouteCommand command) { - return CityBusRouteType.findRoutes(command.depart(), command.arrive()) - .stream() - .flatMap(route -> getScheduleForRoute(route, command)) + return CITY_BUS_INFO.entrySet().stream() + .map(entry -> getScheduleForRoute(entry.getKey(), command.depart(), entry.getValue())) + .flatMap(route -> route.getScheduleInfo(command.date(), command.depart()).stream()) .toList(); } - private Stream getScheduleForRoute(CityBusRouteType route, BusRouteCommand command) { - CityBusTimetable cityBusSchedule = cityBusTimetableRepository - .getByBusInfoNumberAndBusInfoArrival(route.getBusNumber(), route.getArrivalStation().getName()); - - cityBusSchedule.filterBusTimeTablesByDayOfWeek(command.date()); - - // 시내버스 도착 시간 보정(기점 ~ 한기대 정류장 까지의 시간 추가) - List adjustedTimetable = cityBusSchedule.getBusTimetables() - .stream() - .findFirst() - .get() - .applyTimeOffset(route.getBusNumber(), route.getArrivalStation(), command.depart()); - - return toScheduleInfo(adjustedTimetable, "city", route.getBusNumber().toString()).stream(); + private CityBusTimetable getScheduleForRoute(Long busNumber, BusStation depart, CityBusDirection cityBusInfo) { + if (depart == BusStation.TERMINAL) { + return cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(busNumber, "종합터미널"); + } else { + return cityBusTimetableRepository.getByBusInfoNumberAndBusInfoDepart(busNumber, cityBusInfo.getName()); + } } @Override From cd81e3b205767e3ca46117bd718879d8a08307b0 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 16:53:28 +0900 Subject: [PATCH 09/27] =?UTF-8?q?feat=20:=20=EB=8C=80=EC=84=B1=20=EB=B2=84?= =?UTF-8?q?=EC=8A=A4=20=EC=8A=A4=EC=BC=80=EC=A4=84=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../express/ExpressBusRouteSchedule.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusRouteSchedule.java diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusRouteSchedule.java b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusRouteSchedule.java new file mode 100644 index 000000000..c29773587 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusRouteSchedule.java @@ -0,0 +1,46 @@ +package in.koreatech.koin.domain.bus.model.express; + +import java.time.LocalTime; +import java.util.List; + +public final class ExpressBusRouteSchedule { + private static final List 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 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 getExpressBusScheduleToKoreaTech() { + return KOREA_TECH_SCHEDULE; + } + + public static List getExpressBusScheduleToTerminal() { + return TERMINAL_SCHEDULE; + } +} From 69ed6b8c7bda6248c805cadab772d9c43fc7e374 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 16:54:19 +0900 Subject: [PATCH 10/27] =?UTF-8?q?feat=20:=20service=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/service/BusService.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 6eb49f5c1..21d6790f8 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 @@ -9,6 +9,7 @@ import java.time.ZonedDateTime; import java.time.format.TextStyle; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; @@ -19,6 +20,9 @@ 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.BusScheduleResponse.ScheduleInfo; import in.koreatech.koin.domain.bus.dto.BusTimetableResponse; import in.koreatech.koin.domain.bus.dto.CityBusTimetableResponse; import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse; @@ -37,6 +41,7 @@ import in.koreatech.koin.domain.bus.model.mongo.Route; import in.koreatech.koin.domain.bus.repository.BusRepository; import in.koreatech.koin.domain.bus.repository.CityBusTimetableRepository; +import in.koreatech.koin.domain.bus.service.route.BusRouteStrategy; import in.koreatech.koin.domain.bus.util.city.CityBusClient; import in.koreatech.koin.domain.bus.util.city.CityBusRouteClient; import in.koreatech.koin.domain.bus.util.express.ExpressBusService; @@ -57,6 +62,7 @@ public class BusService { private final ExpressBusService expressBusService; private final CityBusRouteClient cityBusRouteClient; private final VersionService versionService; + private final List busRouteStrategies; @Transactional public BusRemainTimeResponse getBusRemainTime(BusType busType, BusStation depart, BusStation arrival) { @@ -229,4 +235,22 @@ public CityBusTimetableResponse getCityBusTimetable(Long busNumber, CityBusDirec return CityBusTimetableResponse.from(timetable); } + + public BusScheduleResponse getBusSchedule(BusRouteCommand request) { + List scheduleInfoList = Collections.emptyList(); + + if (request.checkAvailableCourse()) { + scheduleInfoList = busRouteStrategies.stream() + .filter(strategy -> strategy.support(request.busRouteType())) + .flatMap(strategy -> strategy.findSchedule(request).stream()) + .filter(schedule -> schedule.departTime().isAfter(request.time())) + .sorted(Comparator.comparing(ScheduleInfo::departTime)) + .toList(); + } + + return new BusScheduleResponse( + request.depart(), request.arrive(), request.date(), request.time(), + scheduleInfoList + ); + } } From e8f80cf8ce80317015e3e5766ac4c5c03c099bc0 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 16:54:32 +0900 Subject: [PATCH 11/27] =?UTF-8?q?feat=20:=20controller=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/bus/controller/BusController.java | 16 ++++++++++++ .../domain/bus/model/enums/BusRouteType.java | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/bus/model/enums/BusRouteType.java 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 f74ce989f..b2ed917ba 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,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; @@ -82,4 +85,17 @@ public ResponseEntity> getSearchTimetable( depart, arrival); return ResponseEntity.ok().body(singleBusTimeResponses); } + + @GetMapping("/route") + public ResponseEntity 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); + } } diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/enums/BusRouteType.java b/src/main/java/in/koreatech/koin/domain/bus/model/enums/BusRouteType.java new file mode 100644 index 000000000..372913f0d --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/model/enums/BusRouteType.java @@ -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(); + } +} From 552b76327b116184e340ad7ee5d61cfcaf6422c7 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 19:44:07 +0900 Subject: [PATCH 12/27] =?UTF-8?q?chore=20:=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...eSchedule.java => ExpressBusSchedule.java} | 2 +- .../route/ExpressBusRouteStrategy.java | 39 ++++++------------- 2 files changed, 12 insertions(+), 29 deletions(-) rename src/main/java/in/koreatech/koin/domain/bus/model/express/{ExpressBusRouteSchedule.java => ExpressBusSchedule.java} (96%) diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusRouteSchedule.java b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java similarity index 96% rename from src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusRouteSchedule.java rename to src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java index c29773587..a4da61957 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusRouteSchedule.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java @@ -3,7 +3,7 @@ import java.time.LocalTime; import java.util.List; -public final class ExpressBusRouteSchedule { +public final class ExpressBusSchedule { private static final List KOREA_TECH_SCHEDULE = List.of( LocalTime.of(7, 0), LocalTime.of(8, 30), diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java index 5758e731c..c9c07bf21 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java @@ -1,14 +1,8 @@ package in.koreatech.koin.domain.bus.service.route; -import static in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo.toScheduleInfo; - import java.time.LocalTime; import java.util.Collections; -import java.util.EnumMap; -import java.util.EnumSet; import java.util.List; -import java.util.Map; -import java.util.Set; import org.springframework.stereotype.Component; @@ -17,24 +11,22 @@ import in.koreatech.koin.domain.bus.model.enums.BusDirection; 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.express.ExpressBusRouteSchedule; +import in.koreatech.koin.domain.bus.model.express.ExpressBusSchedule; @Component public class ExpressBusRouteStrategy implements BusRouteStrategy{ - private static final Map> VALID_ROUTES = createValidRoutes(); private static final String BUS_TYPE = "express"; private static final String BUS_NAME = "대성티엔이"; @Override public List findSchedule(BusRouteCommand command) { - if (!isValidRoute(command.depart(), command.arrive())) { - return Collections.emptyList(); - } - + if(validCourse(command.depart(), command.arrive())) return Collections.emptyList(); BusDirection direction = getRouteDirection(command.depart(), command.arrive()); - List departTimes = getStaticExpressBusScheduleTimeList(direction); - return toScheduleInfo(departTimes, BUS_TYPE, BUS_NAME); + + return getStaticExpressBusScheduleTimeList(direction).stream() + .map(time -> new ScheduleInfo(BUS_TYPE, BUS_NAME, time)) + .toList(); } @Override @@ -42,10 +34,10 @@ public boolean support(BusRouteType type) { return type == BusRouteType.EXPRESS || type == BusRouteType.ALL; } - private static List getStaticExpressBusScheduleTimeList(BusDirection direction) { + private List getStaticExpressBusScheduleTimeList(BusDirection direction) { return switch (direction) { - case NORTH -> ExpressBusRouteSchedule.getExpressBusScheduleToKoreaTech(); - case SOUTH -> ExpressBusRouteSchedule.getExpressBusScheduleToTerminal(); + case NORTH -> ExpressBusSchedule.getExpressBusScheduleToKoreaTech(); + case SOUTH -> ExpressBusSchedule.getExpressBusScheduleToTerminal(); }; } @@ -54,16 +46,7 @@ private BusDirection getRouteDirection(BusStation depart, BusStation arrive) { ? BusDirection.NORTH : BusDirection.SOUTH; } - private boolean isValidRoute(BusStation depart, BusStation arrive) { - return VALID_ROUTES.getOrDefault(depart, Collections.emptySet()).contains(arrive); - } - - private static Map> createValidRoutes() { - Map> routes = new EnumMap<>(BusStation.class); - - routes.put(BusStation.KOREATECH, EnumSet.of(BusStation.TERMINAL)); - routes.put(BusStation.TERMINAL, EnumSet.of(BusStation.KOREATECH)); - - return Collections.unmodifiableMap(routes); + private boolean validCourse(BusStation depart, BusStation arrive) { + return (depart == BusStation.STATION || arrive == BusStation.STATION); } } From 65a66b94722e367dc505c2c20037cc9b420eac21 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 20:14:36 +0900 Subject: [PATCH 13/27] =?UTF-8?q?refactor=20:=20=EC=85=94=ED=8B=80?= =?UTF-8?q?=EB=B2=84=EC=8A=A4=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route/NormalShuttleBusStrategy.java | 35 ------------------- ...tegy.java => ShuttleBusRouteStrategy.java} | 13 ++++--- 2 files changed, 6 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java rename src/main/java/in/koreatech/koin/domain/bus/service/route/{CircularShuttleBusStrategy.java => ShuttleBusRouteStrategy.java} (72%) diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java deleted file mode 100644 index 7539e10f5..000000000 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/NormalShuttleBusStrategy.java +++ /dev/null @@ -1,35 +0,0 @@ -package in.koreatech.koin.domain.bus.service.route; - -import java.util.List; - -import org.springframework.stereotype.Service; - -import in.koreatech.koin.domain.bus.dto.BusRouteCommand; -import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; -import in.koreatech.koin.domain.bus.model.enums.BusRouteType; -import in.koreatech.koin.domain.bus.repository.BusRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class NormalShuttleBusStrategy implements BusRouteStrategy{ - - private final BusRepository busRepository; - - @Override - public List findSchedule(BusRouteCommand command) { - String direction = command.depart().getQueryName() == "한기대" ? "from" : "to"; - return busRepository.getByBusTypeAndDirectionAndRegion("shuttle", direction, "천안").getRoutes() - .stream() - .filter(route -> route.filterRoutesByDayOfWeek(command.date())) - .filter(route -> !route.filterCircularRoutes()) - .filter(route -> route.filterDepartAndArriveNode(command.depart(), command.arrive())) - .map(route -> route.getShuttleBusScheduleInfo(command.depart())) - .toList(); - } - - @Override - public boolean support(BusRouteType type) { - return type == BusRouteType.SHUTTLE || type == BusRouteType.ALL; - } -} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CircularShuttleBusStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java similarity index 72% rename from src/main/java/in/koreatech/koin/domain/bus/service/route/CircularShuttleBusStrategy.java rename to src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java index d1dc778ad..be02b8c7a 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/CircularShuttleBusStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java @@ -5,27 +5,26 @@ import org.springframework.stereotype.Service; import in.koreatech.koin.domain.bus.dto.BusRouteCommand; -import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse; import in.koreatech.koin.domain.bus.model.enums.BusRouteType; import in.koreatech.koin.domain.bus.model.mongo.BusCourse; -import in.koreatech.koin.domain.bus.model.mongo.Route; import in.koreatech.koin.domain.bus.repository.BusRepository; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor -public class CircularShuttleBusStrategy implements BusRouteStrategy{ - +public class ShuttleBusRouteStrategy implements BusRouteStrategy{ private final BusRepository busRepository; + private static final String BUS_TYPE = "shuttle"; + private static final String REGION = "천안"; @Override - public List findSchedule(BusRouteCommand command) { - return busRepository.findByBusTypeAndRegion("shuttle", "천안").stream() + public List findSchedule(BusRouteCommand command) { + return busRepository.findByBusTypeAndRegion(BUS_TYPE, REGION).stream() .map(BusCourse::getRoutes) .flatMap(routes -> routes.stream() .filter(route -> route.filterRoutesByDayOfWeek(command.date())) - .filter(Route::filterCircularRoutes) .filter(route -> route.filterDepartAndArriveNode(command.depart(), command.arrive())) .map(route -> route.getShuttleBusScheduleInfo(command.depart())) ) From fca0a9009591651b18ba71252a1841a386e8bc50 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Tue, 26 Nov 2024 20:15:11 +0900 Subject: [PATCH 14/27] =?UTF-8?q?chore=20:=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=83=81=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreatech/koin/domain/bus/dto/BusScheduleResponse.java | 2 +- .../in/koreatech/koin/domain/bus/model/mongo/Route.java | 2 +- ...usRouteStrategy.java => CommutingBusRouteStrategy.java} | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) rename src/main/java/in/koreatech/koin/domain/bus/service/route/{CommutingShuttleBusRouteStrategy.java => CommutingBusRouteStrategy.java} (83%) diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java index 11ec65194..28c229774 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java @@ -59,7 +59,7 @@ public record ScheduleInfo( ) { public static List toScheduleInfo(List timetable, String busType, String busName) { return timetable.stream() - .map(time -> new BusScheduleResponse.ScheduleInfo(busType, busName, time)) + .map(time -> new ScheduleInfo(busType, busName, time)) .collect(Collectors.toList()); } } diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java index 2887487fa..656f66ee6 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java @@ -113,7 +113,7 @@ public ScheduleInfo getCommutingShuttleBusScheduleInfo(BusStation depart) { String busType = "한기대".equals(depart.getQueryName()) ? "하교셔틀" : "등교셔틀"; ArrivalNode findDepartNode = findArrivalNodeByStation(depart); - return new ScheduleInfo("shuttle", String.format("%s, %s", routeName, busType), + return new ScheduleInfo("shuttle", String.format("%s %s", routeName, busType), LocalTime.parse(findDepartNode.getArrivalTime())); } diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingShuttleBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingBusRouteStrategy.java similarity index 83% rename from src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingShuttleBusRouteStrategy.java rename to src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingBusRouteStrategy.java index 38b8128b2..bd4a35cc7 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingShuttleBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingBusRouteStrategy.java @@ -5,7 +5,6 @@ import org.springframework.stereotype.Service; import in.koreatech.koin.domain.bus.dto.BusRouteCommand; -import in.koreatech.koin.domain.bus.dto.BusScheduleResponse; import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; import in.koreatech.koin.domain.bus.model.enums.BusRouteType; import in.koreatech.koin.domain.bus.model.mongo.BusCourse; @@ -14,13 +13,15 @@ @Service @RequiredArgsConstructor -public class CommutingShuttleBusRouteStrategy implements BusRouteStrategy{ +public class CommutingBusRouteStrategy implements BusRouteStrategy{ private final BusRepository busRepository; + private static final String BUS_TYPE = "commuting"; + private static final String REGION = "천안"; @Override public List findSchedule(BusRouteCommand command) { - return busRepository.findByBusTypeAndRegion("commuting", "천안").stream() + return busRepository.findByBusTypeAndRegion(BUS_TYPE, REGION).stream() .map(BusCourse::getRoutes) .flatMap(routes -> routes.stream() From bb908b5685de2030b432742f3bad6812b8fabcc6 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Thu, 28 Nov 2024 14:00:19 +0900 Subject: [PATCH 15/27] =?UTF-8?q?chore=20:=20static=20import=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/bus/service/route/ShuttleBusRouteStrategy.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java index be02b8c7a..a993a2f0a 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Service; import in.koreatech.koin.domain.bus.dto.BusRouteCommand; -import in.koreatech.koin.domain.bus.dto.BusScheduleResponse; +import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; import in.koreatech.koin.domain.bus.model.enums.BusRouteType; import in.koreatech.koin.domain.bus.model.mongo.BusCourse; import in.koreatech.koin.domain.bus.repository.BusRepository; @@ -19,7 +19,7 @@ public class ShuttleBusRouteStrategy implements BusRouteStrategy{ private static final String REGION = "천안"; @Override - public List findSchedule(BusRouteCommand command) { + public List findSchedule(BusRouteCommand command) { return busRepository.findByBusTypeAndRegion(BUS_TYPE, REGION).stream() .map(BusCourse::getRoutes) .flatMap(routes -> From a8646e4f56baa5b1ee5f42d9fc63ba28063df88f Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Fri, 29 Nov 2024 02:11:32 +0900 Subject: [PATCH 16/27] =?UTF-8?q?chore=20:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81=20-=20indent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/model/express/ExpressBusSchedule.java | 1 + .../koin/domain/bus/service/route/BusRouteStrategy.java | 1 + .../domain/bus/service/route/CommutingBusRouteStrategy.java | 2 +- .../koin/domain/bus/service/route/ExpressBusRouteStrategy.java | 2 +- .../koin/domain/bus/service/route/ShuttleBusRouteStrategy.java | 3 ++- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java index a4da61957..b7566cde0 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java @@ -4,6 +4,7 @@ import java.util.List; public final class ExpressBusSchedule { + private static final List KOREA_TECH_SCHEDULE = List.of( LocalTime.of(7, 0), LocalTime.of(8, 30), diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java index 162424287..332f89b67 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/BusRouteStrategy.java @@ -7,6 +7,7 @@ import in.koreatech.koin.domain.bus.model.enums.BusRouteType; public interface BusRouteStrategy { + List findSchedule(BusRouteCommand command); boolean support(BusRouteType type); } diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingBusRouteStrategy.java index bd4a35cc7..c4cd89298 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/CommutingBusRouteStrategy.java @@ -13,7 +13,7 @@ @Service @RequiredArgsConstructor -public class CommutingBusRouteStrategy implements BusRouteStrategy{ +public class CommutingBusRouteStrategy implements BusRouteStrategy { private final BusRepository busRepository; private static final String BUS_TYPE = "commuting"; diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java index c9c07bf21..8e66519fe 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/ExpressBusRouteStrategy.java @@ -14,7 +14,7 @@ import in.koreatech.koin.domain.bus.model.express.ExpressBusSchedule; @Component -public class ExpressBusRouteStrategy implements BusRouteStrategy{ +public class ExpressBusRouteStrategy implements BusRouteStrategy { private static final String BUS_TYPE = "express"; private static final String BUS_NAME = "대성티엔이"; diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java index a993a2f0a..7a86b3970 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/ShuttleBusRouteStrategy.java @@ -13,7 +13,8 @@ @Service @RequiredArgsConstructor -public class ShuttleBusRouteStrategy implements BusRouteStrategy{ +public class ShuttleBusRouteStrategy implements BusRouteStrategy { + private final BusRepository busRepository; private static final String BUS_TYPE = "shuttle"; private static final String REGION = "천안"; From 475534c4c244089bfe7745cbb6ff255871136b13 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Fri, 6 Dec 2024 15:16:22 +0900 Subject: [PATCH 17/27] =?UTF-8?q?feat=20:=20=EB=B2=84=EC=8A=A4=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/dto/BusScheduleResponse.java | 9 +++++---- .../in/koreatech/koin/domain/bus/service/BusService.java | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java index 28c229774..72e58f64e 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.time.LocalTime; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -57,10 +58,10 @@ public record ScheduleInfo( String busName, LocalTime departTime ) { - public static List toScheduleInfo(List timetable, String busType, String busName) { - return timetable.stream() - .map(time -> new ScheduleInfo(busType, busName, time)) - .collect(Collectors.toList()); + + public static Comparator compareBusType() { + List priority = List.of("shuttle", "express", "city"); + return Comparator.comparingInt(schedule -> priority.indexOf(schedule.busType)); } } } 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 21d6790f8..0bad5142f 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 @@ -244,7 +244,9 @@ public BusScheduleResponse getBusSchedule(BusRouteCommand request) { .filter(strategy -> strategy.support(request.busRouteType())) .flatMap(strategy -> strategy.findSchedule(request).stream()) .filter(schedule -> schedule.departTime().isAfter(request.time())) - .sorted(Comparator.comparing(ScheduleInfo::departTime)) + .sorted(Comparator.comparing(ScheduleInfo::departTime) + .thenComparing(ScheduleInfo.compareBusType()) + ) .toList(); } From ef1ef9e1f4d686ab804f608a73ea4fc649ce7d10 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Fri, 6 Dec 2024 15:36:07 +0900 Subject: [PATCH 18/27] =?UTF-8?q?feat=20:=20Api=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/controller/BusApi.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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 f14fd61a6..077440da8 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 @@ -3,6 +3,7 @@ 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; @@ -10,10 +11,12 @@ 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; @@ -90,4 +93,30 @@ ResponseEntity> getSearchTimetable( @Operation(summary = "버스 노선 조회") @GetMapping("/courses") ResponseEntity> getBusCourses(); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation( + summary = "버스 교통편 조회", + description = """ + ### 버스 교통편 조회 + - **시간** : 00:00 인 경우 해당 날짜의 모든 스케줄을 조회합니다. + - **날짜** : 요일을 기준으로 스케줄을 출력합니다. 공휴일 처리는 구현되어 있지 않습니다. + - **출발지 & 도착지** : 출발지와 도착지가 일치하는 경우 빈 리스트를 반환합니다. (천안역 -> 터미널) & (터미널 -> 천안역) 역시 빈 리스트를 반환합니다. + """ + ) + @GetMapping("/route") + ResponseEntity getBusRouteSchedule( + @Parameter(description = "yyyy-MM-dd") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date, + @Parameter(description = "HH:mm") @RequestParam String time, + @Parameter( + description = "CITY, EXPRESS, SHUTTLE, ALL" + ) @RequestParam BusRouteType busRouteType, + @Parameter(description = "KOREATECH, TERMINAL, STATION") @RequestParam BusStation depart, + @Parameter(description = "KOREATECH, TERMINAL, STATION") @RequestParam BusStation arrival + ); } From 187fbd2d00d61346937e61d821b0968435aea92c Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Wed, 11 Dec 2024 00:25:33 +0900 Subject: [PATCH 19/27] =?UTF-8?q?chore=20:=20=EC=A3=BC=EC=84=9D,=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/controller/BusApi.java | 10 +++---- .../domain/bus/dto/BusScheduleResponse.java | 28 +++---------------- .../bus/model/express/ExpressBusSchedule.java | 10 +++++++ .../bus/model/mongo/CityBusTimetable.java | 8 ++++-- 4 files changed, 23 insertions(+), 33 deletions(-) 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 077440da8..ebd7ec69d 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 @@ -104,7 +104,7 @@ ResponseEntity> getSearchTimetable( summary = "버스 교통편 조회", description = """ ### 버스 교통편 조회 - - **시간** : 00:00 인 경우 해당 날짜의 모든 스케줄을 조회합니다. + - **시간** : 13:00 인 경우 13시 이후 출발하는 버스의 시간표를 조회합니다. 00:00 인 경우 해당 날짜의 모든 스케줄을 조회합니다. - **날짜** : 요일을 기준으로 스케줄을 출력합니다. 공휴일 처리는 구현되어 있지 않습니다. - **출발지 & 도착지** : 출발지와 도착지가 일치하는 경우 빈 리스트를 반환합니다. (천안역 -> 터미널) & (터미널 -> 천안역) 역시 빈 리스트를 반환합니다. """ @@ -113,10 +113,8 @@ ResponseEntity> getSearchTimetable( ResponseEntity getBusRouteSchedule( @Parameter(description = "yyyy-MM-dd") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date, @Parameter(description = "HH:mm") @RequestParam String time, - @Parameter( - description = "CITY, EXPRESS, SHUTTLE, ALL" - ) @RequestParam BusRouteType busRouteType, - @Parameter(description = "KOREATECH, TERMINAL, STATION") @RequestParam BusStation depart, - @Parameter(description = "KOREATECH, TERMINAL, STATION") @RequestParam BusStation arrival + @RequestParam BusRouteType busRouteType, + @RequestParam BusStation depart, + @RequestParam BusStation arrival ); } diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java index 72e58f64e..c2f96ba19 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java @@ -25,37 +25,17 @@ public record BusScheduleResponse( 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) + @Schema(description = "교통편 조회 결과", requiredMode = NOT_REQUIRED) List 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 ) { diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java index b7566cde0..dd796bce9 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/express/ExpressBusSchedule.java @@ -3,8 +3,15 @@ import java.time.LocalTime; import java.util.List; +/** + * 한기대와 천안터미널 사이를 운행하는 대성 고속버스의 운행 스케줄을 정적인 데이터로 저장한 클래스입니다. + * 외부 API 가 동작하지 않는 이슈의 해결 전까지 임시적으로 사용하기 위해 작성되었습니다. + */ public final class ExpressBusSchedule { + /** + * 천안 터미널 -> 한기대 출발 시간 + */ private static final List KOREA_TECH_SCHEDULE = List.of( LocalTime.of(7, 0), LocalTime.of(8, 30), @@ -21,6 +28,9 @@ public final class ExpressBusSchedule { LocalTime.of(20, 30) ); + /** + * 한기대 -> 천안 터미널 출발 시간 + */ private static final List TERMINAL_SCHEDULE = List.of( LocalTime.of(8, 35), LocalTime.of(10, 35), diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java index 195bd1f8a..22ada9dc8 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java @@ -24,10 +24,14 @@ @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; - private static final Integer ADDITIONAL_TIME_DEPART_TO_STATION = 7; @Id @Field("_id") @@ -110,8 +114,6 @@ public List applyTimeOffset(Long busNumber, BusStation depart) { 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; }) From 0552b560c9ea7050b8060079cc83f00a9d9658e8 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Wed, 11 Dec 2024 00:31:01 +0900 Subject: [PATCH 20/27] =?UTF-8?q?chore=20:=20=EC=A3=BC=EC=84=9D,=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/dto/BusScheduleResponse.java | 1 - .../koin/domain/bus/model/mongo/CityBusTimetable.java | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java index c2f96ba19..73f368b41 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusScheduleResponse.java @@ -7,7 +7,6 @@ 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; diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java index 22ada9dc8..5096fe355 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/CityBusTimetable.java @@ -27,11 +27,16 @@ public class CityBusTimetable { /** * 1. 한기대 -> 터미널행 시내버스의 기점은 한기대 정류장이 아님 * 2. 400번은 병천3리, 402번은 황사동, 405번은 유관순열사사적지에서 출발해서 한기대 정류장에 도착 - * 3. 제공받는 시내버스의 운행 시간은 기점 기준이기 때문에 각 버스의 기점에서 한기대 정류장에 도착하기까지의 시간을 더해서 보정해야 함 + * 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") @@ -114,6 +119,8 @@ public List applyTimeOffset(Long busNumber, BusStation depart) { 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; }) From bc8289366cdd01f5b96e99b9416a6d759ab1494d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A4=80=EA=B8=B0?= <112807640+dradnats1012@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:18:23 +0900 Subject: [PATCH 21/27] =?UTF-8?q?refactor:=20=EC=97=91=EC=85=80=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC=20(#1041)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 엑셀 다운로드 리팩토링 * feat: 엑셀 다운로드 리팩토링 * refactor: 동시성 제어 어노테이션 삭제 * fix: 리뷰 반영 * fix: 주석 삭제 --- .../koin/domain/coop/service/CoopService.java | 171 +++++++++--------- 1 file changed, 82 insertions(+), 89 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java index ee3daac70..9e72a3b09 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java +++ b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java @@ -22,8 +22,6 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.VerticalAlignment; import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.xssf.streaming.SXSSFCell; -import org.apache.poi.xssf.streaming.SXSSFRow; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.springframework.context.ApplicationEventPublisher; @@ -69,6 +67,7 @@ public class CoopService { private final PasswordEncoder passwordEncoder; private final JwtProvider jwtProvider; + public static final LocalDate LIMIT_DATE = LocalDate.of(2022, 11, 29); private final int EXCEL_COLUMN_COUNT = 8; @Transactional @@ -120,124 +119,118 @@ public CoopLoginResponse coopLogin(CoopLoginRequest request) { } public ByteArrayInputStream generateDiningExcel(LocalDate startDate, LocalDate endDate, Boolean isCafeteria) { - checkDate(startDate, endDate); - - List dinings; - - if (isCafeteria) { - List placeFilters = Arrays.asList("A코너", "B코너", "C코너"); - dinings = diningRepository.findByDateBetweenAndPlaceIn(startDate, endDate, placeFilters); - } else { - dinings = diningRepository.findByDateBetween(startDate, endDate); - } + validateDates(startDate, endDate); + List dinings = fetchDiningData(startDate, endDate, isCafeteria); try (SXSSFWorkbook workbook = new SXSSFWorkbook()) { - SXSSFSheet sheet = workbook.createSheet("식단 메뉴"); - sheet.setRandomAccessWindowSize(100); + SXSSFSheet sheet = createSheet(workbook, "식단 메뉴"); + CellStyle headerStyle = createHeaderStyle(workbook); + CellStyle commonStyle = createCommonStyle(workbook); - CellStyle headerStyle = makeHeaderStyle(workbook); - CellStyle commonStyle = makeCommonStyle(workbook); - createHeaderCell(sheet, headerStyle); - - ByteArrayInputStream result = putDiningData(dinings, sheet, commonStyle, workbook); - - return result; + addHeaderRow(sheet, headerStyle); + addDiningDataToSheet(dinings, sheet, commonStyle); + return writeWorkbookToStream(workbook); } catch (IOException e) { throw new RuntimeException("엑셀 파일 생성 중 오류가 발생했습니다.", e); } } - private static void checkDate(LocalDate startDate, LocalDate endDate) { - LocalDate limitDate = LocalDate.of(2022, 11, 29); - if (startDate.isBefore(limitDate) || endDate.isBefore(limitDate)) { + private void validateDates(LocalDate startDate, LocalDate endDate) { + LocalDate today = LocalDate.now(); + + if (startDate.isBefore(LIMIT_DATE) || endDate.isBefore(LIMIT_DATE)) { throw new DiningLimitDateException("2022/11/29 식단부터 다운받을 수 있어요."); } - - LocalDate now = LocalDate.now(); - if (startDate.isAfter(now) || endDate.isAfter(now)) { + if (startDate.isAfter(today) || endDate.isAfter(today)) { throw new DiningNowDateException("오늘 날짜 이후 기간은 설정할 수 없어요."); } - if (startDate.isAfter(endDate)) { throw new StartDateAfterEndDateException("시작일은 종료일 이전으로 설정해주세요."); } } - private ByteArrayInputStream putDiningData(List dinings, SXSSFSheet sheet, CellStyle commonStyle, - SXSSFWorkbook workbook) throws IOException { - AtomicInteger rowIdx = new AtomicInteger(1); + private List fetchDiningData(LocalDate startDate, LocalDate endDate, Boolean isCafeteria) { + if (isCafeteria) { + List cafeteriaPlaces = Arrays.asList("A코너", "B코너", "C코너"); + return diningRepository.findByDateBetweenAndPlaceIn(startDate, endDate, cafeteriaPlaces); + } + return diningRepository.findByDateBetween(startDate, endDate); + } + + private SXSSFSheet createSheet(SXSSFWorkbook workbook, String sheetName) { + SXSSFSheet sheet = workbook.createSheet(sheetName); + sheet.setRandomAccessWindowSize(100); + return sheet; + } + + private CellStyle createHeaderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + font.setColor(IndexedColors.WHITE.getIndex()); + style.setFont(font); + style.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setAlignment(HorizontalAlignment.CENTER); + return style; + } + + private CellStyle createCommonStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(true); + return style; + } + + private void addHeaderRow(Sheet sheet, CellStyle headerStyle) { + String[] headers = {"날짜", "타입", "코너", "칼로리", "메뉴", "이미지", "품절 여부", "변경 여부"}; + Row headerRow = sheet.createRow(0); + + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerStyle); + } + } + private void addDiningDataToSheet(List dinings, SXSSFSheet sheet, CellStyle commonStyle) { + AtomicInteger rowIndex = new AtomicInteger(1); dinings.forEach(dining -> { - SXSSFRow row = sheet.createRow(rowIdx.getAndIncrement()); - row.createCell(0).setCellValue(dining.getDate().toString()); - row.createCell(1).setCellValue(dining.getType().getDiningName()); - row.createCell(2).setCellValue(dining.getPlace()); - row.createCell(3).setCellValue(dining.getKcal() != null ? dining.getKcal() : 0); - - String formattedMenu = dining.getMenu().toString() - .replaceAll("^\\[|\\]$", "") - .replaceAll(", ", "\n"); - - SXSSFCell menuCell = row.createCell(4); - menuCell.setCellValue(formattedMenu); - - row.createCell(5).setCellValue(dining.getImageUrl()); - row.createCell(6).setCellValue( - Optional.ofNullable(dining.getSoldOut()).map(Object::toString).orElse("") - ); - row.createCell(7).setCellValue(Optional.ofNullable(dining.getIsChanged()).map(Object::toString).orElse("")); - - for (int i = 0; i < EXCEL_COLUMN_COUNT; i++) { - row.getCell(i).setCellStyle(commonStyle); - } + Row row = sheet.createRow(rowIndex.getAndIncrement()); + fillDiningRow(dining, row, commonStyle); }); for (int i = 0; i < EXCEL_COLUMN_COUNT; i++) { sheet.setColumnWidth(i, 6000); } - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - workbook.write(out); - workbook.close(); - workbook.dispose(); - return new ByteArrayInputStream(out.toByteArray()); } - private void createHeaderCell(Sheet sheet, CellStyle headerStyle) { - Row headerRow = sheet.createRow(0); - headerRow.createCell(0).setCellValue("날짜"); - headerRow.createCell(1).setCellValue("타입"); - headerRow.createCell(2).setCellValue("코너"); - headerRow.createCell(3).setCellValue("칼로리"); - headerRow.createCell(4).setCellValue("메뉴"); - headerRow.createCell(5).setCellValue("이미지"); - headerRow.createCell(6).setCellValue("품절 여부"); - headerRow.createCell(7).setCellValue("변경 여부"); - - for (int i = 0; i < EXCEL_COLUMN_COUNT; i++) { - Cell cell = headerRow.getCell(i); - cell.setCellStyle(headerStyle); + private void fillDiningRow(Dining dining, Row row, CellStyle commonStyle) { + row.createCell(0).setCellValue(dining.getDate().toString()); + row.createCell(1).setCellValue(dining.getType().getDiningName()); + row.createCell(2).setCellValue(dining.getPlace()); + row.createCell(3).setCellValue(Optional.ofNullable(dining.getKcal()).orElse(0)); + row.createCell(4).setCellValue(formatMenu(dining.getMenu())); + row.createCell(5).setCellValue(dining.getImageUrl()); + row.createCell(6).setCellValue(Optional.ofNullable(dining.getSoldOut()).map(Object::toString).orElse("")); + row.createCell(7).setCellValue(Optional.ofNullable(dining.getIsChanged()).map(Object::toString).orElse("")); + + for (int i = 0; i < 8; i++) { + row.getCell(i).setCellStyle(commonStyle); } } - private static CellStyle makeCommonStyle(Workbook workbook) { - CellStyle commonStyle = workbook.createCellStyle(); - commonStyle.setAlignment(HorizontalAlignment.CENTER); - commonStyle.setVerticalAlignment(VerticalAlignment.CENTER); - commonStyle.setWrapText(true); - return commonStyle; + private String formatMenu(List menu) { + return String.join("\n", menu); } - private static CellStyle makeHeaderStyle(Workbook workbook) { - CellStyle headerStyle = workbook.createCellStyle(); - Font font = workbook.createFont(); - font.setBold(true); - font.setColor(IndexedColors.WHITE.getIndex()); - headerStyle.setFont(font); - headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); - headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - headerStyle.setAlignment(HorizontalAlignment.CENTER); - return headerStyle; + private ByteArrayInputStream writeWorkbookToStream(SXSSFWorkbook workbook) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + workbook.write(out); + workbook.dispose(); + return new ByteArrayInputStream(out.toByteArray()); + } } } From 031e93853f8f9e91cabc8281036fd0892d6984c2 Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Wed, 11 Dec 2024 15:13:13 +0900 Subject: [PATCH 22/27] =?UTF-8?q?fix=20:=20=EC=9D=BC=EB=B6=80=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=97=90=20=EB=8C=80=ED=95=B4=20404=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/bus/model/mongo/Route.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java index 656f66ee6..240c0a498 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/Route.java @@ -77,26 +77,14 @@ public boolean filterRoutesByDayOfWeek(LocalDate date) { return runningDays.contains(dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.US).toUpperCase()); } - public boolean filterCircularRoutes() { - if (arrivalInfos.isEmpty()) { - return false; - } - - String firstNodeName = arrivalInfos.get(0).getNodeName(); - String lastNodeName = arrivalInfos.get(arrivalInfos.size() - 1).getNodeName(); - - return firstNodeName.equals(lastNodeName); - } - public boolean filterDepartAndArriveNode(BusStation departNode, BusStation arriveNode) { boolean foundDepart = false; for (ArrivalNode node : arrivalInfos) { - if (!foundDepart && node.getNodeName().contains(departNode.getQueryName())) { + if (!foundDepart && node.getNodeName().contains(departNode.getQueryName()) + && isValidTimeFormat(node.getArrivalTime())) { foundDepart = true; - } - - else if (foundDepart && node.getNodeName().contains(arriveNode.getQueryName())) { + } else if (foundDepart && node.getNodeName().contains(arriveNode.getQueryName())) { return true; } } @@ -124,6 +112,12 @@ private ArrivalNode findArrivalNodeByStation(BusStation depart) { .orElseThrow(() -> new BusArrivalNodeNotFoundException("")); } + private boolean isValidTimeFormat(String time) { + // HH:mm 형식의 정규식 (00:00부터 23:59까지 유효) + String timeRegex = "([01]\\d|2[0-3]):[0-5]\\d"; + return time != null && time.matches(timeRegex); + } + @Builder private Route(String routeName, List runningDays, List arrivalInfos) { this.routeName = routeName; From 2c4291bc9833ad93ffde45e1906652bffbe5fa9b Mon Sep 17 00:00:00 2001 From: DHkimgit Date: Wed, 11 Dec 2024 15:14:44 +0900 Subject: [PATCH 23/27] =?UTF-8?q?feat=20:=20=EC=B2=9C=EC=95=88=EC=97=AD=20?= =?UTF-8?q?<->=20=ED=84=B0=EB=AF=B8=EB=84=90=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A0=9C=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/in/koreatech/koin/domain/bus/controller/BusApi.java | 2 +- .../in/koreatech/koin/domain/bus/dto/BusRouteCommand.java | 5 +---- .../koin/domain/bus/service/route/CityBusRouteStrategy.java | 4 ++++ 3 files changed, 6 insertions(+), 5 deletions(-) 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 ebd7ec69d..3d203fc15 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 @@ -106,7 +106,7 @@ ResponseEntity> getSearchTimetable( ### 버스 교통편 조회 - **시간** : 13:00 인 경우 13시 이후 출발하는 버스의 시간표를 조회합니다. 00:00 인 경우 해당 날짜의 모든 스케줄을 조회합니다. - **날짜** : 요일을 기준으로 스케줄을 출력합니다. 공휴일 처리는 구현되어 있지 않습니다. - - **출발지 & 도착지** : 출발지와 도착지가 일치하는 경우 빈 리스트를 반환합니다. (천안역 -> 터미널) & (터미널 -> 천안역) 역시 빈 리스트를 반환합니다. + - **출발지 & 도착지** : 출발지와 도착지가 일치하는 경우 빈 리스트를 반환합니다. """ ) @GetMapping("/route") diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java index 75e31a390..5e423b857 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusRouteCommand.java @@ -16,9 +16,6 @@ public record BusRouteCommand( ) { 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; + return depart != arrive; } } diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java b/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java index 7c19695fe..d4eb4cd2e 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/route/CityBusRouteStrategy.java @@ -1,5 +1,6 @@ package in.koreatech.koin.domain.bus.service.route; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -27,6 +28,9 @@ public class CityBusRouteStrategy implements BusRouteStrategy { @Override public List findSchedule(BusRouteCommand command) { + if (command.depart() == BusStation.STATION && command.arrive() == BusStation.TERMINAL) + return Collections.emptyList(); + return CITY_BUS_INFO.entrySet().stream() .map(entry -> getScheduleForRoute(entry.getKey(), command.depart(), entry.getValue())) .flatMap(route -> route.getScheduleInfo(command.date(), command.depart()).stream()) From 610fee0716feb378cf53d09a4b74bd10a2a1e3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=ED=98=84?= <149303551+krSeonghyeon@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:55:54 +0900 Subject: [PATCH 24/27] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=98=9C=ED=83=9D=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=ED=98=9C=ED=83=9D=EB=AC=B8=EA=B5=AC=20=EB=AF=B8?= =?UTF-8?q?=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EC=B6=94=EA=B0=80=20(#1132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 특정 혜택 카테고리 상점 전체 조회에 혜택 문구 미리보기 추가 반환 * feat: 특정 혜택 카테고리 상점 전체 추가에 혜택 문구 미리보기 추가 * feat: 특정 혜택 카테고리 상점 수정 API 추가 * test: 특정 혜택 카테고리의 상점 전체 조회 테스트 추가 * test: 특정 혜택 카테고리의 상점 추가 테스트 작성 * test: 특정 혜택 카테고리의 상점들 수정 테스트 작성 * chore: 개행추가 * chore: 멀티라인 문자열로 변경 * chore: 멀티라인 문자열로 변경 * feat: 수정에서 shopId가 아닌 mapId를 이용하도록 변경 * feat: 불필요한 schema 삭제 --- .../benefit/controller/AdminBenefitApi.java | 17 +++ .../controller/AdminBenefitController.java | 10 ++ .../dto/AdminBenefitShopsResponse.java | 25 +++-- .../dto/AdminCreateBenefitShopsRequest.java | 21 +++- .../dto/AdminCreateBenefitShopsResponse.java | 21 ++-- .../dto/AdminModifyBenefitShopsRequest.java | 35 ++++++ .../BenefitMapNotFoundException.java | 20 ++++ .../AdminBenefitCategoryMapRepository.java | 16 ++- .../benefit/service/AdminBenefitService.java | 57 ++++++++-- .../benefit/model/BenefitCategoryMap.java | 4 + .../admin/acceptance/AdminBenefitApiTest.java | 100 +++++++++++++++--- 11 files changed, 282 insertions(+), 44 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/admin/benefit/dto/AdminModifyBenefitShopsRequest.java create mode 100644 src/main/java/in/koreatech/koin/admin/benefit/exception/BenefitMapNotFoundException.java diff --git a/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitApi.java b/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitApi.java index 9b0ea821f..094ec84ef 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitApi.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitApi.java @@ -21,6 +21,7 @@ import in.koreatech.koin.admin.benefit.dto.AdminDeleteShopsRequest; import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitCategoryRequest; import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitCategoryResponse; +import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitShopsRequest; import in.koreatech.koin.admin.benefit.dto.AdminSearchBenefitShopsResponse; import in.koreatech.koin.global.auth.Auth; import io.swagger.v3.oas.annotations.Operation; @@ -127,6 +128,22 @@ ResponseEntity createBenefitShops( @Auth(permit = {ADMIN}) Integer adminId ); + @ApiResponses( + value = { + @ApiResponse(responseCode = "201"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "특정 혜택을 제공하는 상점을 수정한다.") + @PutMapping + ResponseEntity modifyBenefitShops( + @RequestBody AdminModifyBenefitShopsRequest request, + @Auth(permit = {ADMIN}) Integer adminId + ); + @ApiResponses( value = { @ApiResponse(responseCode = "204"), diff --git a/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitController.java b/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitController.java index 544180fbc..76577af7f 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitController.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/controller/AdminBenefitController.java @@ -23,6 +23,7 @@ import in.koreatech.koin.admin.benefit.dto.AdminDeleteShopsRequest; import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitCategoryRequest; import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitCategoryResponse; +import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitShopsRequest; import in.koreatech.koin.admin.benefit.dto.AdminSearchBenefitShopsResponse; import in.koreatech.koin.admin.benefit.service.AdminBenefitService; import in.koreatech.koin.global.auth.Auth; @@ -90,6 +91,15 @@ public ResponseEntity createBenefitShops( return ResponseEntity.status(HttpStatus.CREATED).body(response); } + @PutMapping + public ResponseEntity modifyBenefitShops( + @RequestBody AdminModifyBenefitShopsRequest request, + @Auth(permit = {ADMIN}) Integer adminId + ) { + adminBenefitService.modifyBenefitShops(request); + return ResponseEntity.ok().build(); + } + @DeleteMapping("/{id}/shops") public ResponseEntity deleteBenefitShops( @PathVariable("id") Integer benefitId, diff --git a/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminBenefitShopsResponse.java b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminBenefitShopsResponse.java index 8ef41e87a..67779287f 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminBenefitShopsResponse.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminBenefitShopsResponse.java @@ -5,9 +5,11 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import in.koreatech.koin.domain.benefit.model.BenefitCategoryMap; import in.koreatech.koin.domain.shop.model.shop.Shop; import io.swagger.v3.oas.annotations.media.Schema; +@JsonNaming(SnakeCaseStrategy.class) public record AdminBenefitShopsResponse( @Schema(example = "3", description = "상점 개수") Integer count, @@ -16,27 +18,36 @@ public record AdminBenefitShopsResponse( List shops ) { - public static AdminBenefitShopsResponse from(List shops) { + public static AdminBenefitShopsResponse from(List benefitCategoryMaps) { return new AdminBenefitShopsResponse( - shops.size(), - shops.stream() + benefitCategoryMaps.size(), + benefitCategoryMaps.stream() .map(InnerShopResponse::from) .toList() ); } + @JsonNaming(SnakeCaseStrategy.class) private record InnerShopResponse( + @Schema(example = "1", description = "상점혜택 매핑id") + Integer shopBenefitMapId, + @Schema(example = "1", description = "고유 id") Integer id, @Schema(example = "수신반점", description = "이름") - String name + String name, + + @Schema(example = "4인 이상 픽업서비스", description = "혜택 미리보기 문구") + String detail ) { - public static InnerShopResponse from(Shop shop) { + public static InnerShopResponse from(BenefitCategoryMap benefitCategoryMap) { return new InnerShopResponse( - shop.getId(), - shop.getName() + benefitCategoryMap.getId(), + benefitCategoryMap.getShop().getId(), + benefitCategoryMap.getShop().getName(), + benefitCategoryMap.getDetail() ); } } diff --git a/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsRequest.java b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsRequest.java index 35b36ae40..b2a11f935 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsRequest.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsRequest.java @@ -9,14 +9,27 @@ import in.koreatech.koin.global.validation.NotBlankElement; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; @JsonNaming(SnakeCaseStrategy.class) public record AdminCreateBenefitShopsRequest( - @Schema(description = "상점 ID 리스트", example = "[1, 2, 5]", requiredMode = REQUIRED) - @NotNull(message = "상점 ID 리스트는 필수입니다.") - @NotBlankElement(message = "상점 ID 리스트는 빈 요소가 존재할 수 없습니다.") - List shopIds + @NotNull(message = "상점정보 리스트는 필수입니다.") + @NotBlankElement(message = "상점정보 리스트는 빈 요소가 존재할 수 없습니다.") + List shopDetails ) { + @JsonNaming(SnakeCaseStrategy.class) + public record InnerBenefitShopsRequest( + @Schema(description = "상점 고유 id", example = "2", requiredMode = REQUIRED) + @NotNull(message = "상점은 필수입니다.") + Integer shopId, + + @Schema(description = "혜택 미리보기 문구", example = "4인 이상 픽업서비스", requiredMode = REQUIRED) + @NotBlank(message = "혜택 미리보기 문구는 필수입니다.") + @Size(min = 2, max = 15, message = "혜택 미리보기 문구는 최소 2자 최대 15자입니다.") + String detail + ) { + } } diff --git a/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsResponse.java b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsResponse.java index 1f4adf7d9..246d431d1 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsResponse.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminCreateBenefitShopsResponse.java @@ -5,16 +5,19 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import in.koreatech.koin.domain.benefit.model.BenefitCategoryMap; import in.koreatech.koin.domain.shop.model.shop.Shop; import io.swagger.v3.oas.annotations.media.Schema; public record AdminCreateBenefitShopsResponse( - @Schema(description = "상점 리스트") + @Schema(description = "상점 정보") List shops ) { - public static AdminCreateBenefitShopsResponse from(List shops) { + public static AdminCreateBenefitShopsResponse from(List benefitCategoryMaps) { return new AdminCreateBenefitShopsResponse( - shops.stream().map(InnerShopResponse::from).toList() + benefitCategoryMaps.stream() + .map(InnerShopResponse::from) + .toList() ); } @@ -23,13 +26,17 @@ private record InnerShopResponse( Integer id, @Schema(description = "상점 이름", example = "수신반점") - String name + String name, + + @Schema(example = "4인 이상 픽업서비스", description = "혜택 미리보기 문구") + String detail ) { - public static InnerShopResponse from(Shop shop) { + public static InnerShopResponse from(BenefitCategoryMap benefitCategoryMap) { return new InnerShopResponse( - shop.getId(), - shop.getName() + benefitCategoryMap.getShop().getId(), + benefitCategoryMap.getShop().getName(), + benefitCategoryMap.getDetail() ); } } diff --git a/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminModifyBenefitShopsRequest.java b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminModifyBenefitShopsRequest.java new file mode 100644 index 000000000..c0c2614a6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/benefit/dto/AdminModifyBenefitShopsRequest.java @@ -0,0 +1,35 @@ +package in.koreatech.koin.admin.benefit.dto; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.global.validation.NotBlankElement; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@JsonNaming(SnakeCaseStrategy.class) +public record AdminModifyBenefitShopsRequest( + @NotNull(message = "혜택문구 변경정보 리스트는 필수입니다.") + @NotBlankElement(message = "혜택문구 변경정보 리스트는 빈 요소가 존재할 수 없습니다.") + List modifyDetails +) { + + @JsonNaming(SnakeCaseStrategy.class) + public record InnerBenefitShopsRequest( + @Schema(description = "상점혜택 매핑id", example = "2", requiredMode = REQUIRED) + @NotNull(message = "상점혜택 매핑id는 필수입니다.") + Integer shopBenefitMapId, + + @Schema(description = "혜택 미리보기 문구", example = "4인 이상 픽업서비스", requiredMode = REQUIRED) + @NotBlank(message = "혜택 미리보기 문구는 필수입니다.") + @Size(min = 2, max = 15, message = "혜택 미리보기 문구는 최소 2자 최대 15자입니다.") + String detail + ) { + } +} diff --git a/src/main/java/in/koreatech/koin/admin/benefit/exception/BenefitMapNotFoundException.java b/src/main/java/in/koreatech/koin/admin/benefit/exception/BenefitMapNotFoundException.java new file mode 100644 index 000000000..fa6faa242 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/benefit/exception/BenefitMapNotFoundException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.benefit.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class BenefitMapNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "해당 혜택 카테고리에 존재하지 않는 입니다."; + + public BenefitMapNotFoundException(String message) { + super(message); + } + + public BenefitMapNotFoundException(String message, String detail) { + super(message, detail); + } + + public static BenefitMapNotFoundException withDetail(String detail) { + return new BenefitMapNotFoundException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java b/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java index 20f2ba22e..56125d12b 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java @@ -4,14 +4,14 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; +import org.springframework.data.repository.CrudRepository; import in.koreatech.koin.domain.benefit.model.BenefitCategoryMap; import org.springframework.data.repository.query.Param; -public interface AdminBenefitCategoryMapRepository extends Repository { +public interface AdminBenefitCategoryMapRepository extends CrudRepository { - void save(BenefitCategoryMap benefitCategoryMap); + List findAllByIdIn(List ids); @Query(""" SELECT bcm @@ -22,6 +22,16 @@ public interface AdminBenefitCategoryMapRepository extends Repository findAllByBenefitCategoryIdOrderByShopName(@Param("benefitId") Integer benefitId); + @Query(""" + SELECT bcm + FROM BenefitCategoryMap bcm + WHERE bcm.benefitCategory.id = :benefitId AND bcm.shop.id IN :shopIds + """) + List findAllByBenefitCategoryIdAndShopIds( + @Param("benefitId") Integer benefitId, + @Param("shopIds") List shopIds + ); + @Modifying @Query(""" DELETE FROM BenefitCategoryMap bcm diff --git a/src/main/java/in/koreatech/koin/admin/benefit/service/AdminBenefitService.java b/src/main/java/in/koreatech/koin/admin/benefit/service/AdminBenefitService.java index 7b349578e..0a1c04c6c 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/service/AdminBenefitService.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/service/AdminBenefitService.java @@ -1,6 +1,7 @@ package in.koreatech.koin.admin.benefit.service; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -16,9 +17,11 @@ import in.koreatech.koin.admin.benefit.dto.AdminDeleteShopsRequest; import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitCategoryRequest; import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitCategoryResponse; +import in.koreatech.koin.admin.benefit.dto.AdminModifyBenefitShopsRequest; import in.koreatech.koin.admin.benefit.dto.AdminSearchBenefitShopsResponse; import in.koreatech.koin.admin.benefit.exception.BenefitDuplicationException; import in.koreatech.koin.admin.benefit.exception.BenefitLimitException; +import in.koreatech.koin.admin.benefit.exception.BenefitMapNotFoundException; import in.koreatech.koin.admin.benefit.repository.AdminBenefitCategoryMapRepository; import in.koreatech.koin.admin.benefit.repository.AdminBenefitCategoryRepository; import in.koreatech.koin.admin.shop.repository.shop.AdminShopRepository; @@ -90,10 +93,7 @@ public void deleteBenefitCategory(Integer categoryId) { public AdminBenefitShopsResponse getBenefitShops(Integer benefitId) { List benefitCategoryMaps = adminBenefitCategoryMapRepository.findAllByBenefitCategoryIdOrderByShopName(benefitId); - List shops = benefitCategoryMaps.stream() - .map(BenefitCategoryMap::getShop) - .toList(); - return AdminBenefitShopsResponse.from(shops); + return AdminBenefitShopsResponse.from(benefitCategoryMaps); } @Transactional @@ -101,18 +101,53 @@ public AdminCreateBenefitShopsResponse createBenefitShops( Integer benefitId, AdminCreateBenefitShopsRequest request ) { - List shops = adminShopRepository.findAllByIdIn(request.shopIds()); BenefitCategory benefitCategory = adminBenefitCategoryRepository.getById(benefitId); - for (Shop shop : shops) { - BenefitCategoryMap benefitCategoryMap = BenefitCategoryMap.builder() + Map shopIdToDetail = request.shopDetails().stream() + .collect(Collectors.toMap( + AdminCreateBenefitShopsRequest.InnerBenefitShopsRequest::shopId, + AdminCreateBenefitShopsRequest.InnerBenefitShopsRequest::detail + )); + List shops = adminShopRepository.findAllByIdIn(shopIdToDetail.keySet().stream().toList()); + + List benefitCategoryMaps = shops.stream() + .map(shop -> BenefitCategoryMap.builder() .shop(shop) .benefitCategory(benefitCategory) - .build(); - adminBenefitCategoryMapRepository.save(benefitCategoryMap); - } - return AdminCreateBenefitShopsResponse.from(shops); + .detail(shopIdToDetail.get(shop.getId())) + .build() + ) + .toList(); + adminBenefitCategoryMapRepository.saveAll(benefitCategoryMaps); + return AdminCreateBenefitShopsResponse.from(benefitCategoryMaps); + } + + @Transactional + public void modifyBenefitShops(AdminModifyBenefitShopsRequest request) { + Map shopBenefitIdToDetail = request.modifyDetails().stream() + .collect(Collectors.toMap( + AdminModifyBenefitShopsRequest.InnerBenefitShopsRequest::shopBenefitMapId, + AdminModifyBenefitShopsRequest.InnerBenefitShopsRequest::detail + )); + + List benefitCategoryMaps = + adminBenefitCategoryMapRepository.findAllByIdIn(shopBenefitIdToDetail.keySet().stream().toList()); + + validateBenefitMapIds(shopBenefitIdToDetail, benefitCategoryMaps); + benefitCategoryMaps.forEach(map -> map.modifyDetail(shopBenefitIdToDetail.get(map.getId()))); } + private static void validateBenefitMapIds( + Map shopBenefitIdToDetail, + List benefitCategoryMaps + ) { + List notFoundMapIds = shopBenefitIdToDetail.keySet().stream() + .filter(mapId -> benefitCategoryMaps.stream().noneMatch(map -> map.getId().equals(mapId))) + .toList(); + + if (!notFoundMapIds.isEmpty()) { + throw new BenefitMapNotFoundException("해당 혜택 카테고리에 존재하지 않는 상점이 포함되어 있습니다. shopBenefitMapId: " + notFoundMapIds); + } + } @Transactional public void deleteBenefitShops(Integer benefitId, AdminDeleteShopsRequest request) { diff --git a/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java b/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java index f416584cd..d29a0d14c 100644 --- a/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java +++ b/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java @@ -46,4 +46,8 @@ public BenefitCategoryMap(Shop shop, BenefitCategory benefitCategory, String det this.benefitCategory = benefitCategory; this.detail = detail; } + + public void modifyDetail(String detail) { + this.detail = detail; + } } diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminBenefitApiTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminBenefitApiTest.java index 1dafe5c1e..f2b5b6c29 100644 --- a/src/test/java/in/koreatech/koin/admin/acceptance/AdminBenefitApiTest.java +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminBenefitApiTest.java @@ -6,6 +6,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -13,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.admin.benefit.repository.AdminBenefitCategoryMapRepository; @@ -64,6 +67,9 @@ public class AdminBenefitApiTest extends AcceptanceTest { @Autowired ShopNotificationMessageFixture shopNotificationMessageFixture; + @Autowired + private TransactionTemplate transactionTemplate; + Admin admin; String token_admin; Owner 현수_사장님; @@ -73,6 +79,11 @@ public class AdminBenefitApiTest extends AcceptanceTest { BenefitCategory 서비스_증정; BenefitCategory 가게까지_픽업; + BenefitCategoryMap 김밥천국_혜택; + BenefitCategoryMap 마슬랜_혜택; + BenefitCategoryMap 티바_혜택; + BenefitCategoryMap 신전_혜택; + Shop 마슬랜; Shop 김밥천국; Shop 영업중인_티바; @@ -99,10 +110,10 @@ void setup() { 영업중인_티바 = shopFixture.영업중인_티바(현수_사장님); 영업중이_아닌_신전_떡볶이 = shopFixture.영업중이_아닌_신전_떡볶이(현수_사장님); - benefitCategoryMapFixture.혜택_추가(김밥천국, 배달비_무료); - benefitCategoryMapFixture.혜택_추가(마슬랜, 배달비_무료); - benefitCategoryMapFixture.혜택_추가(영업중인_티바, 배달비_무료); - benefitCategoryMapFixture.혜택_추가(영업중이_아닌_신전_떡볶이, 배달비_무료); + 김밥천국_혜택 = benefitCategoryMapFixture.설명이_포함된_혜택_추가(김밥천국, 배달비_무료, "설명1"); + 마슬랜_혜택 = benefitCategoryMapFixture.설명이_포함된_혜택_추가(마슬랜, 배달비_무료, "설명2"); + 티바_혜택 = benefitCategoryMapFixture.설명이_포함된_혜택_추가(영업중인_티바, 배달비_무료, "설명3"); + 신전_혜택 = benefitCategoryMapFixture.설명이_포함된_혜택_추가(영업중이_아닌_신전_떡볶이, 배달비_무료, "설명4"); notificationMessage_가게 = shopNotificationMessageFixture.알림메시지_가게(); shopParentCategory_가게 = shopParentCategoryFixture.상위_카테고리_가게(notificationMessage_가게); @@ -233,24 +244,36 @@ void setup() { "count": 4, "shops": [ { + "shop_benefit_map_id": %d, "id": %d, - "name": "김밥천국" + "name": "김밥천국", + "detail": "설명1" }, { + "shop_benefit_map_id": %d, "id": %d, - "name": "마슬랜 치킨" + "name": "마슬랜 치킨", + "detail": "설명2" }, { + "shop_benefit_map_id": %d, "id": %d, - "name": "티바" + "name": "티바", + "detail": "설명3" }, { + "shop_benefit_map_id": %d, "id": %d, - "name": "신전 떡볶이" + "name": "신전 떡볶이", + "detail": "설명4" } ] } - """, 김밥천국.getId(), 마슬랜.getId(), 영업중인_티바.getId(), 영업중이_아닌_신전_떡볶이.getId()))); + """, + 김밥천국_혜택.getId(), 김밥천국.getId(), + 마슬랜_혜택.getId(), 마슬랜.getId(), + 티바_혜택.getId(), 영업중인_티바.getId(), + 신전_혜택.getId(), 영업중이_아닌_신전_떡볶이.getId()))); } @Test @@ -261,7 +284,16 @@ void setup() { .contentType(MediaType.APPLICATION_JSON) .content(String.format(""" { - "shop_ids": [%d, %d] + "shop_details": [ + { + "shop_id": %d, + "detail": "김밥혜택설명" + }, + { + "shop_id": %d, + "detail": "마슬랜혜택설명" + } + ] } """, 김밥천국.getId(), 마슬랜.getId())) ) @@ -271,17 +303,61 @@ void setup() { "shops": [ { "id": %d, - "name": "김밥천국" + "name": "김밥천국", + "detail": "김밥혜택설명" }, { "id": %d, - "name": "마슬랜 치킨" + "name": "마슬랜 치킨", + "detail": "마슬랜혜택설명" } ] } """, 김밥천국.getId(), 마슬랜.getId()))); } + @Test + void 특정_혜택을_제공하는_상점들을_수정한다() throws Exception { + mockMvc.perform( + put("/admin/benefit") + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "modify_details": [ + { + "shop_benefit_map_id": %d, + "detail": "김밥새혜택설명" + }, + { + "shop_benefit_map_id": %d, + "detail": "마슬랜새혜택설명" + } + ] + } + """, 김밥천국_혜택.getId(), 마슬랜_혜택.getId())) + ) + .andExpect(status().isOk()); + + transactionTemplate.executeWithoutResult(status -> { + List updatedBenefit = + adminBenefitCategoryMapRepository.findAllByIdIn( + List.of(김밥천국_혜택.getId(), 마슬랜_혜택.getId()) + ); + + Map details = updatedBenefit.stream() + .collect(Collectors.toMap( + BenefitCategoryMap::getId, + BenefitCategoryMap::getDetail + )); + + assertThat(details).isEqualTo(Map.of( + 김밥천국_혜택.getId(), "김밥새혜택설명", + 마슬랜_혜택.getId(), "마슬랜새혜택설명" + )); + }); + } + @Test void 특정_혜택을_제공하는_상점들을_삭제한다() throws Exception { mockMvc.perform( From 91f7692d5741a6a3b8b5ef1ddb35e4af16342788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= <164142264+kih1015@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:33:02 +0900 Subject: [PATCH 25/27] =?UTF-8?q?feat:=20=ED=95=99=EA=B5=90=EB=B2=84?= =?UTF-8?q?=EC=8A=A4=20=EC=8B=9C=EA=B0=84=ED=91=9C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?API=20(#1119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 이름 수정 --- .../koin/domain/bus/controller/BusApi.java | 22 ++++++ .../domain/bus/controller/BusController.java | 15 ++++ .../bus/dto/ShuttleBusRoutesResponse.java | 78 +++++++++++++++++++ .../bus/dto/ShuttleBusTimetableResponse.java | 78 +++++++++++++++++++ .../exception/BusIllegalRegionException.java | 20 +++++ .../BusIllegalRouteTypeException.java | 20 +++++ .../bus/model/enums/ShuttleBusRegion.java | 26 +++++++ .../bus/model/enums/ShuttleRouteType.java | 25 ++++++ .../bus/model/mongo/ShuttleBusRoute.java | 72 +++++++++++++++++ .../bus/repository/ShuttleBusRepository.java | 22 ++++++ .../domain/bus/service/ShuttleBusService.java | 34 ++++++++ 11 files changed, 412 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusRoutesResponse.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusTimetableResponse.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRegionException.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRouteTypeException.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleRouteType.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/model/mongo/ShuttleBusRoute.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/repository/ShuttleBusRepository.java create mode 100644 src/main/java/in/koreatech/koin/domain/bus/service/ShuttleBusService.java 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 3d203fc15..c13d74fa8 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 @@ -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; @@ -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; @@ -94,6 +97,25 @@ ResponseEntity> getSearchTimetable( @GetMapping("/courses") ResponseEntity> getBusCourses(); + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + } + ) + @Operation(summary = "학교버스 노선 조회") + @GetMapping("/courses/shuttle") + ResponseEntity 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 getShuttleBusTimetable(@PathVariable String id); + @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 b2ed917ba..bfa8bd567 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 @@ -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; @@ -17,6 +18,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; @@ -24,6 +27,7 @@ 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 @@ -32,6 +36,7 @@ public class BusController implements BusApi { private final BusService busService; + private final ShuttleBusService shuttleBusService; @GetMapping public ResponseEntity getBusRemainTime( @@ -86,6 +91,16 @@ public ResponseEntity> getSearchTimetable( return ResponseEntity.ok().body(singleBusTimeResponses); } + @GetMapping("/courses/shuttle") + public ResponseEntity getShuttleBusRoutes() { + return ResponseEntity.ok().body(shuttleBusService.getShuttleBusRoutes()); + } + + @GetMapping("/timetable/shuttle/{id}") + public ResponseEntity getShuttleBusTimetable(@PathVariable String id) { + return ResponseEntity.ok().body(shuttleBusService.getShuttleBusTimetable(id)); + } + @GetMapping("/route") public ResponseEntity getBusRouteSchedule( @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date, diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusRoutesResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusRoutesResponse.java new file mode 100644 index 000000000..99ea7418b --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusRoutesResponse.java @@ -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 routeRegions, + @Schema(description = "학기 정보") RouteSemester semesterInfo +) { + + @JsonNaming(SnakeCaseStrategy.class) + @Schema(description = "노선 지역 정보") + public record RouteRegion( + @Schema(description = "지역 이름", example = "천안") + String region, + + @Schema(description = "해당 지역의 경로 목록") + List 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 shuttleBusRoutes, + VersionMessageResponse versionMessageResponse) { + List 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 mapCategories(List 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 mapRouteNames(List 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(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusTimetableResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusTimetableResponse.java new file mode 100644 index 000000000..d655df2c9 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/ShuttleBusTimetableResponse.java @@ -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 nodeInfo, + + @Schema(description = "회차 정보 목록") + List 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 arrivalTime + ) { + } + + public static ShuttleBusTimetableResponse from(ShuttleBusRoute shuttleBusRoute) { + List nodeInfoResponses = shuttleBusRoute.getNodeInfo().stream() + .map(node -> new NodeInfoResponse(node.getName(), node.getDetail())) + .toList(); + List 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 + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRegionException.java b/src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRegionException.java new file mode 100644 index 000000000..03b9f3ebd --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRegionException.java @@ -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); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRouteTypeException.java b/src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRouteTypeException.java new file mode 100644 index 000000000..ab007e07b --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/exception/BusIllegalRouteTypeException.java @@ -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); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java b/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java new file mode 100644 index 000000000..758d253e5 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java @@ -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); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleRouteType.java b/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleRouteType.java new file mode 100644 index 000000000..8b41f016e --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleRouteType.java @@ -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); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/mongo/ShuttleBusRoute.java b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/ShuttleBusRoute.java new file mode 100644 index 000000000..d13e68730 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/model/mongo/ShuttleBusRoute.java @@ -0,0 +1,72 @@ +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 in.koreatech.koin.domain.bus.model.enums.ShuttleBusRegion; +import in.koreatech.koin.domain.bus.model.enums.ShuttleRouteType; +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 ShuttleBusRegion region; + + @Field("route_type") + private ShuttleRouteType routeType; + + @Field("route_name") + private String routeName; + + @Field("sub_name") + private String subName; + + @Field("node_info") + private List nodeInfo; + + @Field("route_info") + private List 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("detail") + private String detail; + + @Field("running_days") + private List runningDays; + + @Field("arrival_time") + private List arrivalTime; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/repository/ShuttleBusRepository.java b/src/main/java/in/koreatech/koin/domain/bus/repository/ShuttleBusRepository.java new file mode 100644 index 000000000..2d60c4587 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/repository/ShuttleBusRepository.java @@ -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 { + + List findBySemesterType(String semesterType); + + Optional findById(String id); + + default ShuttleBusRoute getById(String id) { + return findById(id).orElseThrow( + () -> BusNotFoundException.withDetail("id: " + id)); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/ShuttleBusService.java b/src/main/java/in/koreatech/koin/domain/bus/service/ShuttleBusService.java new file mode 100644 index 000000000..731bc729d --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/service/ShuttleBusService.java @@ -0,0 +1,34 @@ +package in.koreatech.koin.domain.bus.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.domain.bus.dto.ShuttleBusRoutesResponse; +import in.koreatech.koin.domain.bus.dto.ShuttleBusTimetableResponse; +import in.koreatech.koin.domain.bus.model.mongo.ShuttleBusRoute; +import in.koreatech.koin.domain.bus.repository.ShuttleBusRepository; +import in.koreatech.koin.domain.version.dto.VersionMessageResponse; +import in.koreatech.koin.domain.version.service.VersionService; +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ShuttleBusService { + + private final ShuttleBusRepository shuttleBusRepository; + private final VersionService versionService; + + public ShuttleBusRoutesResponse getShuttleBusRoutes() { + VersionMessageResponse version = versionService.getVersionWithMessage("shuttle_bus_timetable"); + List shuttleBusRoutes = shuttleBusRepository.findBySemesterType(version.title()); + return ShuttleBusRoutesResponse.of(shuttleBusRoutes, version); + } + + public ShuttleBusTimetableResponse getShuttleBusTimetable(String id) { + ShuttleBusRoute shuttleBusRoute = shuttleBusRepository.getById(id); + return ShuttleBusTimetableResponse.from(shuttleBusRoute); + } +} From 14d9d8e5b9dfd134bf9fad7432808e185322fd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= <164142264+kih1015@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:37:39 +0900 Subject: [PATCH 26/27] =?UTF-8?q?chore:=20=EC=A7=80=EC=97=AD=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=88=98=EC=A0=95=20(#1134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ex: 천안·아산 -> 천안・아산 --- .../koin/domain/bus/model/enums/ShuttleBusRegion.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java b/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java index 758d253e5..cf9551b10 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java +++ b/src/main/java/in/koreatech/koin/domain/bus/model/enums/ShuttleBusRegion.java @@ -7,10 +7,10 @@ @Getter @RequiredArgsConstructor public enum ShuttleBusRegion { - CHEONAN_ASAN("천안·아산"), + CHEONAN_ASAN("천안・아산"), CHEONGJU("청주"), SEOUL("서울"), - DAEJEON_SEJONG("대전·세종"), + DAEJEON_SEJONG("대전・세종"), ; private final String label; From 885875d7ae5fe1cb571b88342aa2e9b66c46d336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Fri, 13 Dec 2024 21:11:29 +0900 Subject: [PATCH 27/27] =?UTF-8?q?fix:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#1131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 반환 날짜 형식 변경 * feat: 검색 정렬 옵션 추가 * fix: String 필드 Enum으로 변경 * style: 코드 포멧팅 * chore: 미사용 import 삭제 * style: 코드 포멧팅 * chore: 코드 블럭 처리 * chore: historys -> histories으로 변경 * chore: 정적 메소드 네이밍 변경 * style: 코드 개행 * style: 코드 개행 * fix: 로깅 어노테이션으로 변경 * Revert "fix: 로깅 어노테이션으로 변경" This reverts commit 7c1ecb22d5fe15b506432089e6019df14ad0ed3e. * chore: 리뷰 반영 * style: 코드 포멧팅 --- .../aop/AdminActivityHistoryAspect.java | 8 +++-- .../admin/history/controller/HistoryApi.java | 15 +++++--- .../history/controller/HistoryController.java | 24 +++++++------ ...tion.java => AdminHistoriesCondition.java} | 34 ++++++++++++++----- ...ponse.java => AdminHistoriesResponse.java} | 26 +++++++------- .../history/dto/AdminHistoryResponse.java | 8 ++--- .../history/model/AdminActivityHistory.java | 22 ++++++++---- .../AdminActivityHistoryRepository.java | 9 ++--- .../admin/history/service/HistoryService.java | 19 ++++++----- 9 files changed, 102 insertions(+), 63 deletions(-) rename src/main/java/in/koreatech/koin/admin/history/dto/{AdminHistorysCondition.java => AdminHistoriesCondition.java} (52%) rename src/main/java/in/koreatech/koin/admin/history/dto/{AdminHistorysResponse.java => AdminHistoriesResponse.java} (77%) diff --git a/src/main/java/in/koreatech/koin/admin/history/aop/AdminActivityHistoryAspect.java b/src/main/java/in/koreatech/koin/admin/history/aop/AdminActivityHistoryAspect.java index 0b784c58d..720ae012c 100644 --- a/src/main/java/in/koreatech/koin/admin/history/aop/AdminActivityHistoryAspect.java +++ b/src/main/java/in/koreatech/koin/admin/history/aop/AdminActivityHistoryAspect.java @@ -12,6 +12,7 @@ import org.springframework.web.util.ContentCachingRequestWrapper; import in.koreatech.koin.admin.history.enums.DomainType; +import in.koreatech.koin.admin.history.enums.HttpMethodType; import in.koreatech.koin.admin.history.model.AdminActivityHistory; import in.koreatech.koin.admin.history.repository.AdminActivityHistoryRepository; import in.koreatech.koin.admin.user.model.Admin; @@ -58,7 +59,7 @@ private void excludeSpecificMethods() { public Object logAdminActivity(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); String requestURI = request.getRequestURI(); - String requestMethod = request.getMethod(); + HttpMethodType requestMethod = HttpMethodType.valueOf(request.getMethod()); ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper)request; String requestMessage = new String(cachingRequest.getContentAsByteArray()); @@ -72,9 +73,10 @@ public Object logAdminActivity(ProceedingJoinPoint joinPoint) throws Throwable { .domainId(domainInfo.domainId()) .admin(admin) .requestMethod(requestMethod) - .domainName(domainInfo.domainName()) + .domainName(DomainType.valueOf(domainInfo.domainName())) .requestMessage(requestMessage) - .build()); + .build() + ); return result; } diff --git a/src/main/java/in/koreatech/koin/admin/history/controller/HistoryApi.java b/src/main/java/in/koreatech/koin/admin/history/controller/HistoryApi.java index 70bdba8fd..fe0edc9b1 100644 --- a/src/main/java/in/koreatech/koin/admin/history/controller/HistoryApi.java +++ b/src/main/java/in/koreatech/koin/admin/history/controller/HistoryApi.java @@ -1,6 +1,7 @@ package in.koreatech.koin.admin.history.controller; import static in.koreatech.koin.domain.user.model.UserType.ADMIN; +import static in.koreatech.koin.global.model.Criteria.Sort; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -8,7 +9,9 @@ import org.springframework.web.bind.annotation.RequestParam; import in.koreatech.koin.admin.history.dto.AdminHistoryResponse; -import in.koreatech.koin.admin.history.dto.AdminHistorysResponse; +import in.koreatech.koin.admin.history.dto.AdminHistoriesResponse; +import in.koreatech.koin.admin.history.enums.DomainType; +import in.koreatech.koin.admin.history.enums.HttpMethodType; import in.koreatech.koin.global.auth.Auth; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -19,6 +22,7 @@ @Tag(name = "(Admin) History: 기록", description = "관리자 기록 관련 API") public interface HistoryApi { + @ApiResponses( value = { @ApiResponse(responseCode = "200"), @@ -29,13 +33,14 @@ public interface HistoryApi { } ) @Operation(summary = "히스토리 리스트 조회") - @GetMapping("/admin/historys") - ResponseEntity getHistorys( + @GetMapping("/admin/histories") + ResponseEntity getHistories( @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer limit, - @RequestParam(required = false) String requestMethod, - @RequestParam(required = false) String domainName, + @RequestParam(required = false) HttpMethodType requestMethod, + @RequestParam(required = false) DomainType domainName, @RequestParam(required = false) Integer domainId, + @RequestParam(required = false) Sort sort, @Auth(permit = {ADMIN}) Integer adminId ); diff --git a/src/main/java/in/koreatech/koin/admin/history/controller/HistoryController.java b/src/main/java/in/koreatech/koin/admin/history/controller/HistoryController.java index efc11bbbf..01aaa834f 100644 --- a/src/main/java/in/koreatech/koin/admin/history/controller/HistoryController.java +++ b/src/main/java/in/koreatech/koin/admin/history/controller/HistoryController.java @@ -1,6 +1,7 @@ package in.koreatech.koin.admin.history.controller; import static in.koreatech.koin.domain.user.model.UserType.ADMIN; +import static in.koreatech.koin.global.model.Criteria.Sort; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -9,8 +10,10 @@ import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.admin.history.dto.AdminHistoryResponse; -import in.koreatech.koin.admin.history.dto.AdminHistorysCondition; -import in.koreatech.koin.admin.history.dto.AdminHistorysResponse; +import in.koreatech.koin.admin.history.dto.AdminHistoriesCondition; +import in.koreatech.koin.admin.history.dto.AdminHistoriesResponse; +import in.koreatech.koin.admin.history.enums.DomainType; +import in.koreatech.koin.admin.history.enums.HttpMethodType; import in.koreatech.koin.admin.history.service.HistoryService; import in.koreatech.koin.global.auth.Auth; import lombok.RequiredArgsConstructor; @@ -21,19 +24,20 @@ public class HistoryController implements HistoryApi { private final HistoryService historyService; - @GetMapping("/admin/historys") - public ResponseEntity getHistorys( + @GetMapping("/admin/histories") + public ResponseEntity getHistories( @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer limit, - @RequestParam(required = false) String requestMethod, - @RequestParam(required = false) String domainName, + @RequestParam(required = false) HttpMethodType requestMethod, + @RequestParam(required = false) DomainType domainName, @RequestParam(required = false) Integer domainId, + @RequestParam(required = false) Sort sort, @Auth(permit = {ADMIN}) Integer adminId ) { - AdminHistorysCondition adminHistorysCondition = new AdminHistorysCondition(page, limit, requestMethod, - domainName, domainId); - AdminHistorysResponse historys = historyService.getHistorys(adminHistorysCondition); - return ResponseEntity.ok(historys); + AdminHistoriesCondition adminHistoriesCondition = new AdminHistoriesCondition(page, limit, requestMethod, + domainName, domainId, sort); + AdminHistoriesResponse histories = historyService.getHistories(adminHistoriesCondition); + return ResponseEntity.ok(histories); } @GetMapping("/admin/history/{id}") diff --git a/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistorysCondition.java b/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoriesCondition.java similarity index 52% rename from src/main/java/in/koreatech/koin/admin/history/dto/AdminHistorysCondition.java rename to src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoriesCondition.java index e738e5b10..e7e775554 100644 --- a/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistorysCondition.java +++ b/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoriesCondition.java @@ -1,17 +1,22 @@ package in.koreatech.koin.admin.history.dto; import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static in.koreatech.koin.global.model.Criteria.*; +import static in.koreatech.koin.global.model.Criteria.Sort.CREATED_AT_ASC; +import static in.koreatech.koin.global.model.Criteria.Sort.CREATED_AT_DESC; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static org.springframework.data.domain.Sort.Direction; import java.util.Objects; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import in.koreatech.koin.global.model.Criteria; +import in.koreatech.koin.admin.history.enums.DomainType; +import in.koreatech.koin.admin.history.enums.HttpMethodType; import io.swagger.v3.oas.annotations.media.Schema; @JsonNaming(value = SnakeCaseStrategy.class) -public record AdminHistorysCondition( +public record AdminHistoriesCondition( @Schema(description = "페이지", example = "1", defaultValue = "1", requiredMode = NOT_REQUIRED) Integer page, @@ -19,20 +24,33 @@ public record AdminHistorysCondition( Integer limit, @Schema(description = "HTTP 메소드", example = "POST", requiredMode = NOT_REQUIRED) - String requestMethod, + HttpMethodType requestMethod, @Schema(description = "도메인 이름", example = "NOTICE", requiredMode = NOT_REQUIRED) - String domainName, + DomainType domainName, @Schema(description = "특정 엔티티 id", requiredMode = NOT_REQUIRED) - Integer domainId + Integer domainId, + + @Schema(description = "정렬 기준", requiredMode = NOT_REQUIRED) + Sort sort ) { - public AdminHistorysCondition { + public AdminHistoriesCondition { if (Objects.isNull(page)) { - page = Criteria.DEFAULT_PAGE; + page = DEFAULT_PAGE; } if (Objects.isNull(limit)) { - limit = Criteria.DEFAULT_LIMIT; + limit = DEFAULT_LIMIT; + } + if (Objects.isNull(sort)) { + sort = CREATED_AT_DESC; + } + } + + public Direction getSortDir() { + if (sort == CREATED_AT_ASC) { + return Direction.ASC; } + return Direction.DESC; } } diff --git a/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistorysResponse.java b/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoriesResponse.java similarity index 77% rename from src/main/java/in/koreatech/koin/admin/history/dto/AdminHistorysResponse.java rename to src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoriesResponse.java index 26891b308..61e1a8722 100644 --- a/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistorysResponse.java +++ b/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoriesResponse.java @@ -12,13 +12,11 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import in.koreatech.koin.admin.history.enums.DomainType; -import in.koreatech.koin.admin.history.enums.HttpMethodType; import in.koreatech.koin.admin.history.model.AdminActivityHistory; import io.swagger.v3.oas.annotations.media.Schema; @JsonNaming(value = SnakeCaseStrategy.class) -public record AdminHistorysResponse( +public record AdminHistoriesResponse( @Schema(description = "조건에 해당하는 히스토리 수", example = "10", requiredMode = REQUIRED) Long totalCount, @@ -32,10 +30,10 @@ public record AdminHistorysResponse( Integer currentPage, @Schema(description = "어드민 계정 리스트", requiredMode = REQUIRED) - List historys + List histories ) { @JsonNaming(value = SnakeCaseStrategy.class) - public record InnerAdminHistorysResponse( + public record InnerAdminHistoriesResponse( @Schema(description = "고유 id", example = "1", requiredMode = REQUIRED) Integer id, @@ -61,31 +59,31 @@ public record InnerAdminHistorysResponse( ) String requestMessage, - @Schema(description = "요청 시간", example = "2019-08-16-23-01-52", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy-MM-dd-HH-mm-ss") + @Schema(description = "요청 시간", example = "2019-08-16 23:01:52", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt ) { - public static InnerAdminHistorysResponse from(AdminActivityHistory adminActivityHistory) { - return new InnerAdminHistorysResponse( + public static InnerAdminHistoriesResponse from(AdminActivityHistory adminActivityHistory) { + return new InnerAdminHistoriesResponse( adminActivityHistory.getId(), adminActivityHistory.getDomainId(), adminActivityHistory.getAdmin().getUser().getName(), - DomainType.valueOf(adminActivityHistory.getDomainName()).getDescription(), - HttpMethodType.valueOf(adminActivityHistory.getRequestMethod()).getValue(), + adminActivityHistory.getDomainName().getDescription(), + adminActivityHistory.getRequestMethod().getValue(), adminActivityHistory.getRequestMessage(), adminActivityHistory.getCreatedAt() ); } } - public static AdminHistorysResponse of(Page adminActivityHistoryPage) { - return new AdminHistorysResponse( + public static AdminHistoriesResponse from(Page adminActivityHistoryPage) { + return new AdminHistoriesResponse( adminActivityHistoryPage.getTotalElements(), adminActivityHistoryPage.getContent().size(), adminActivityHistoryPage.getTotalPages(), adminActivityHistoryPage.getNumber() + 1, adminActivityHistoryPage.getContent().stream() - .map(InnerAdminHistorysResponse::from) + .map(InnerAdminHistoriesResponse::from) .collect(Collectors.toList()) ); } diff --git a/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoryResponse.java b/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoryResponse.java index 406ce536a..035fa6e59 100644 --- a/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoryResponse.java +++ b/src/main/java/in/koreatech/koin/admin/history/dto/AdminHistoryResponse.java @@ -40,8 +40,8 @@ public record AdminHistoryResponse( ) String requestMessage, - @Schema(description = "요청 시간", example = "2019-08-16-23-01-52", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy-MM-dd-HH-mm-ss") + @Schema(description = "요청 시간", example = "2019-08-16 23:01:52", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt ) { public static AdminHistoryResponse from(AdminActivityHistory adminActivityHistory) { @@ -49,8 +49,8 @@ public static AdminHistoryResponse from(AdminActivityHistory adminActivityHistor adminActivityHistory.getId(), adminActivityHistory.getDomainId(), adminActivityHistory.getAdmin().getUser().getName(), - DomainType.valueOf(adminActivityHistory.getDomainName()).getDescription(), - HttpMethodType.valueOf(adminActivityHistory.getRequestMethod()).getValue(), + adminActivityHistory.getDomainName().getDescription(), + adminActivityHistory.getRequestMethod().getValue(), adminActivityHistory.getRequestMessage(), adminActivityHistory.getCreatedAt() ); diff --git a/src/main/java/in/koreatech/koin/admin/history/model/AdminActivityHistory.java b/src/main/java/in/koreatech/koin/admin/history/model/AdminActivityHistory.java index 3ab600a04..afb88e2b4 100644 --- a/src/main/java/in/koreatech/koin/admin/history/model/AdminActivityHistory.java +++ b/src/main/java/in/koreatech/koin/admin/history/model/AdminActivityHistory.java @@ -3,17 +3,20 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; +import in.koreatech.koin.admin.history.enums.DomainType; +import in.koreatech.koin.admin.history.enums.HttpMethodType; import in.koreatech.koin.admin.user.model.Admin; import in.koreatech.koin.global.domain.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -32,14 +35,14 @@ public class AdminActivityHistory extends BaseEntity { private Integer domainId; @NotNull - @Size(max = 10) + @Enumerated(value = EnumType.STRING) @Column(name = "request_method", nullable = false, length = 10) - private String requestMethod; + private HttpMethodType requestMethod; @NotNull - @Size(max = 20) + @Enumerated(value = EnumType.STRING) @Column(name = "domain_name", nullable = false, length = 20) - private String domainName; + private DomainType domainName; @Column(name = "request_message", columnDefinition = "TEXT") private String requestMessage; @@ -49,8 +52,13 @@ public class AdminActivityHistory extends BaseEntity { private Admin admin; @Builder - public AdminActivityHistory(Integer domainId, String requestMethod, String domainName, String requestMessage, - Admin admin) { + public AdminActivityHistory( + Integer domainId, + HttpMethodType requestMethod, + DomainType domainName, + String requestMessage, + Admin admin + ) { this.domainId = domainId; this.requestMethod = requestMethod; this.domainName = domainName; diff --git a/src/main/java/in/koreatech/koin/admin/history/repository/AdminActivityHistoryRepository.java b/src/main/java/in/koreatech/koin/admin/history/repository/AdminActivityHistoryRepository.java index eb0a9721f..01c6cf937 100644 --- a/src/main/java/in/koreatech/koin/admin/history/repository/AdminActivityHistoryRepository.java +++ b/src/main/java/in/koreatech/koin/admin/history/repository/AdminActivityHistoryRepository.java @@ -8,11 +8,12 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; -import in.koreatech.koin.admin.history.dto.AdminHistorysCondition; +import in.koreatech.koin.admin.history.dto.AdminHistoriesCondition; import in.koreatech.koin.admin.history.exception.AdminActivityHistoryNotFoundException; import in.koreatech.koin.admin.history.model.AdminActivityHistory; public interface AdminActivityHistoryRepository extends Repository { + AdminActivityHistory save(AdminActivityHistory adminActivityHistory); Optional findById(Integer id); @@ -27,10 +28,10 @@ default AdminActivityHistory getById(Integer id) { @Query(""" SELECT a FROM AdminActivityHistory a WHERE - (:#{#condition.requestMethod} IS NULL OR a.requestMethod = :#{#condition.requestMethod}) AND - (:#{#condition.domainName} IS NULL OR a.domainName = :#{#condition.domainName}) AND + (:#{#condition.requestMethod?.name()} IS NULL OR a.requestMethod = :#{#condition.requestMethod}) AND + (:#{#condition.domainName?.name()} IS NULL OR a.domainName = :#{#condition.domainName}) AND (:#{#condition.domainId} IS NULL OR a.domainId = :#{#condition.domainId}) """) - Page findByConditions(@Param("condition") AdminHistorysCondition adminsCondition, + Page findByConditions(@Param("condition") AdminHistoriesCondition adminsCondition, Pageable pageable); } diff --git a/src/main/java/in/koreatech/koin/admin/history/service/HistoryService.java b/src/main/java/in/koreatech/koin/admin/history/service/HistoryService.java index 84706bab5..124973a64 100644 --- a/src/main/java/in/koreatech/koin/admin/history/service/HistoryService.java +++ b/src/main/java/in/koreatech/koin/admin/history/service/HistoryService.java @@ -2,11 +2,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import in.koreatech.koin.admin.history.dto.AdminHistoryResponse; -import in.koreatech.koin.admin.history.dto.AdminHistorysCondition; -import in.koreatech.koin.admin.history.dto.AdminHistorysResponse; +import in.koreatech.koin.admin.history.dto.AdminHistoriesCondition; +import in.koreatech.koin.admin.history.dto.AdminHistoriesResponse; import in.koreatech.koin.admin.history.model.AdminActivityHistory; import in.koreatech.koin.admin.history.repository.AdminActivityHistoryRepository; import in.koreatech.koin.global.model.Criteria; @@ -18,16 +19,18 @@ public class HistoryService { private final AdminActivityHistoryRepository adminActivityHistoryRepository; - public AdminHistorysResponse getHistorys(AdminHistorysCondition condition) { + public AdminHistoriesResponse getHistories(AdminHistoriesCondition condition) { Integer total = adminActivityHistoryRepository.countAdminActivityHistory(); Criteria criteria = Criteria.of(condition.page(), condition.limit(), total); - PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit()); - Page adminActivityHistoryRepositoryPage = adminActivityHistoryRepository.findByConditions( - condition, - pageRequest); + PageRequest pageRequest = PageRequest.of( + criteria.getPage(), criteria.getLimit(), + Sort.by(condition.getSortDir(), "createdAt") + ); + Page adminActivityHistoryPage = adminActivityHistoryRepository.findByConditions( + condition, pageRequest); - return AdminHistorysResponse.of(adminActivityHistoryRepositoryPage); + return AdminHistoriesResponse.from(adminActivityHistoryPage); } public AdminHistoryResponse getHistory(Integer id) {