diff --git a/src/main/java/wooteco/subway/admin/SubwayAdminApplication.java b/src/main/java/wooteco/subway/admin/SubwayAdminApplication.java index 22fe640db..e34ff3a5e 100644 --- a/src/main/java/wooteco/subway/admin/SubwayAdminApplication.java +++ b/src/main/java/wooteco/subway/admin/SubwayAdminApplication.java @@ -2,6 +2,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing; @SpringBootApplication public class SubwayAdminApplication { diff --git a/src/main/java/wooteco/subway/admin/config/ETagHeaderFilter.java b/src/main/java/wooteco/subway/admin/config/ETagHeaderFilter.java index 1f855da15..89bd7a193 100644 --- a/src/main/java/wooteco/subway/admin/config/ETagHeaderFilter.java +++ b/src/main/java/wooteco/subway/admin/config/ETagHeaderFilter.java @@ -1,5 +1,20 @@ package wooteco.subway.admin.config; -// TODO: ETag 관련 설정하기 +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +@Configuration public class ETagHeaderFilter { + + @Bean + public FilterRegistrationBean + shallowEtagHeaderFilter() { + FilterRegistrationBean filter + = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + filter.addUrlPatterns("/lines/detail"); + filter.setName("etagFilter"); + return filter; + } } diff --git a/src/main/java/wooteco/subway/admin/config/JdbcAuditConfiguration.java b/src/main/java/wooteco/subway/admin/config/JdbcAuditConfiguration.java new file mode 100644 index 000000000..f80fdb258 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/config/JdbcAuditConfiguration.java @@ -0,0 +1,9 @@ +package wooteco.subway.admin.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing; + +@Configuration +@EnableJdbcAuditing +public class JdbcAuditConfiguration { +} diff --git a/src/main/java/wooteco/subway/admin/controller/GlobalExceptionHandler.java b/src/main/java/wooteco/subway/admin/controller/GlobalExceptionHandler.java new file mode 100644 index 000000000..3b6f0d975 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/controller/GlobalExceptionHandler.java @@ -0,0 +1,47 @@ +package wooteco.subway.admin.controller; + +import java.util.stream.Collectors; + +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import wooteco.subway.admin.dto.ErrorResponse; +import wooteco.subway.admin.exception.BusinessException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(value = MethodArgumentNotValidException.class) + public ResponseEntity validExceptionHandler(MethodArgumentNotValidException e) { + BindingResult bindingResult = e.getBindingResult(); + String errorMessage = bindingResult.getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining("\n")); + return ResponseEntity.badRequest().body(new ErrorResponse(errorMessage)); + } + + @ExceptionHandler(value = BusinessException.class) + public ResponseEntity bindingErrorHandler(BusinessException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler(value = {DataAccessException.class}) + public ResponseEntity bindingErrorHandler(DataAccessException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler(value = Exception.class) + public ResponseEntity defaultExceptionHandler(Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(e.getMessage())); + } +} diff --git a/src/main/java/wooteco/subway/admin/controller/LineController.java b/src/main/java/wooteco/subway/admin/controller/LineController.java index d445c21af..4ace36d3f 100644 --- a/src/main/java/wooteco/subway/admin/controller/LineController.java +++ b/src/main/java/wooteco/subway/admin/controller/LineController.java @@ -1,65 +1,93 @@ package wooteco.subway.admin.controller; +import java.net.URI; +import java.util.List; + +import javax.validation.Valid; + +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import wooteco.subway.admin.domain.Line; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + import wooteco.subway.admin.dto.LineDetailResponse; import wooteco.subway.admin.dto.LineRequest; import wooteco.subway.admin.dto.LineResponse; import wooteco.subway.admin.dto.LineStationCreateRequest; +import wooteco.subway.admin.dto.WholeSubwayResponse; import wooteco.subway.admin.service.LineService; -import java.net.URI; -import java.util.List; - @RestController +@RequestMapping("/lines") public class LineController { - private LineService lineService; + private final LineService lineService; public LineController(LineService lineService) { this.lineService = lineService; } - @PostMapping(value = "/lines") - public ResponseEntity createLine(@RequestBody LineRequest view) { - Line persistLine = lineService.save(view.toLine()); + @PostMapping + public ResponseEntity create(@Valid @RequestBody LineRequest request) { + LineResponse lineResponse = lineService.save(request.toLine()); return ResponseEntity - .created(URI.create("/lines/" + persistLine.getId())) - .body(LineResponse.of(persistLine)); + .created(URI.create("/lines/" + lineResponse.getId())) + .body(lineResponse); } - @GetMapping("/lines") - public ResponseEntity> showLine() { - return ResponseEntity.ok().body(LineResponse.listOf(lineService.showLines())); + @GetMapping + public ResponseEntity> retrieveLines() { + return ResponseEntity.ok().body(lineService.findLines()); } - @GetMapping("/lines/{id}") - public ResponseEntity retrieveLine(@PathVariable Long id) { - return ResponseEntity.ok().body(lineService.findLineWithStationsById(id)); + @GetMapping("/{id}") + public ResponseEntity retrieveLine(@PathVariable Long id) { + return ResponseEntity.ok().body(lineService.findLine(id)); } - @PutMapping("/lines/{id}") - public ResponseEntity updateLine(@PathVariable Long id, @RequestBody LineRequest view) { - lineService.updateLine(id, view); + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, + @RequestBody LineRequest request) { + lineService.updateLine(id, request); return ResponseEntity.ok().build(); } - @DeleteMapping("/lines/{id}") - public ResponseEntity deleteLine(@PathVariable Long id) { + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { lineService.deleteLineById(id); return ResponseEntity.noContent().build(); } - @PostMapping("/lines/{lineId}/stations") - public ResponseEntity addLineStation(@PathVariable Long lineId, @RequestBody LineStationCreateRequest view) { - lineService.addLineStation(lineId, view); + @PostMapping("/{lineId}/stations") + public ResponseEntity addLineStation(@PathVariable Long lineId, + @Valid @RequestBody LineStationCreateRequest request) { + lineService.addLineStation(lineId, request); return ResponseEntity.ok().build(); } - @DeleteMapping("/lines/{lineId}/stations/{stationId}") - public ResponseEntity removeLineStation(@PathVariable Long lineId, @PathVariable Long stationId) { + @DeleteMapping("/{lineId}/stations/{stationId}") + public ResponseEntity removeLineStation(@PathVariable Long lineId, + @PathVariable Long stationId) { lineService.removeLineStation(lineId, stationId); return ResponseEntity.noContent().build(); } + + @GetMapping("/detail") + public ResponseEntity retrieveDetailLines() { + WholeSubwayResponse response = lineService.findDetailLines(); + return ResponseEntity.ok().body(response); + } + + @GetMapping("/detail/{id}") + public ResponseEntity retrieveDetailLine(@PathVariable Long id) { + LineDetailResponse response = lineService.findDetailLine(id); + + return ResponseEntity.status(HttpStatus.OK).body(response); + } } diff --git a/src/main/java/wooteco/subway/admin/controller/PageController.java b/src/main/java/wooteco/subway/admin/controller/PageController.java index a8e4b2ebb..178ade5df 100644 --- a/src/main/java/wooteco/subway/admin/controller/PageController.java +++ b/src/main/java/wooteco/subway/admin/controller/PageController.java @@ -2,40 +2,50 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import wooteco.subway.admin.repository.StationRepository; -import wooteco.subway.admin.service.LineService; +import org.springframework.web.bind.annotation.RequestMapping; @Controller +@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public class PageController { - private LineService lineService; - private StationRepository stationRepository; - public PageController(LineService lineService, StationRepository stationRepository) { - this.lineService = lineService; - this.stationRepository = stationRepository; + @GetMapping + public String index() { + return "index"; } - @GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE) - public String index() { + @GetMapping(value = "/service") + public String serviceIndex() { + return "service/index"; + } + + @GetMapping(value = "/service/map") + public String map() { + return "service/map"; + } + + @GetMapping(value = "/service/search") + public String search() { + return "service/search"; + } + + @GetMapping(value = "/admin") + public String adminIndex() { return "admin/index"; } - @GetMapping(value = "/stations", produces = MediaType.TEXT_HTML_VALUE) - public String stationPage(Model model) { - model.addAttribute("stations", stationRepository.findAll()); + @GetMapping(value = "/admin/station") + public String stationPage() { return "admin/admin-station"; } - @GetMapping(value = "/lines", produces = MediaType.TEXT_HTML_VALUE) - public String linePage(Model model) { - model.addAttribute("lines", lineService.showLines()); + @GetMapping(value = "/admin/line") + public String linePage() { return "admin/admin-line"; } - @GetMapping(value = "/edges", produces = MediaType.TEXT_HTML_VALUE) - public String edgePage(Model model) { + @GetMapping(value = "/admin/edge") + public String edgePage() { return "admin/admin-edge"; } } diff --git a/src/main/java/wooteco/subway/admin/controller/PathController.java b/src/main/java/wooteco/subway/admin/controller/PathController.java new file mode 100644 index 000000000..b2f36f87e --- /dev/null +++ b/src/main/java/wooteco/subway/admin/controller/PathController.java @@ -0,0 +1,30 @@ +package wooteco.subway.admin.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import wooteco.subway.admin.dto.PathRequest; +import wooteco.subway.admin.dto.PathResponse; +import wooteco.subway.admin.service.PathService; + +@RestController +@RequestMapping("/path") +public class PathController { + + private final PathService pathService; + + public PathController(PathService pathService) { + this.pathService = pathService; + } + + @GetMapping + ResponseEntity findPath(@RequestParam String sourceName, + @RequestParam String targetName, @RequestParam String type) { + PathResponse response = pathService.findPath(new PathRequest(sourceName, targetName, type)); + + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/wooteco/subway/admin/controller/StationController.java b/src/main/java/wooteco/subway/admin/controller/StationController.java index 780ecaa91..4599fa621 100644 --- a/src/main/java/wooteco/subway/admin/controller/StationController.java +++ b/src/main/java/wooteco/subway/admin/controller/StationController.java @@ -1,41 +1,53 @@ package wooteco.subway.admin.controller; +import java.net.URI; +import java.util.List; + +import javax.validation.Valid; + import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import wooteco.subway.admin.domain.Station; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + import wooteco.subway.admin.dto.StationCreateRequest; import wooteco.subway.admin.dto.StationResponse; -import wooteco.subway.admin.repository.StationRepository; - -import java.net.URI; -import java.util.List; +import wooteco.subway.admin.service.StationService; @RestController +@RequestMapping("/stations") public class StationController { - private final StationRepository stationRepository; + private final StationService stationService; - public StationController(StationRepository stationRepository) { - this.stationRepository = stationRepository; + public StationController(StationService stationService) { + this.stationService = stationService; } - @PostMapping("/stations") - public ResponseEntity createStation(@RequestBody StationCreateRequest view) { - Station station = view.toStation(); - Station persistStation = stationRepository.save(station); + @PostMapping + public ResponseEntity create( + @Valid @RequestBody StationCreateRequest request) { + StationResponse response = stationService.save(request); return ResponseEntity - .created(URI.create("/stations/" + persistStation.getId())) - .body(StationResponse.of(persistStation)); + .created(URI.create("/stations/" + response.getId())) + .body(response); } - @GetMapping("/stations") - public ResponseEntity> showStations() { - return ResponseEntity.ok().body(StationResponse.listOf(stationRepository.findAll())); + @GetMapping + public ResponseEntity> get() { + List responses = stationService.findAll(); + + return ResponseEntity.ok(responses); } - @DeleteMapping("/stations/{id}") - public ResponseEntity deleteStation(@PathVariable Long id) { - stationRepository.deleteById(id); + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + stationService.deleteById(id); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/wooteco/subway/admin/domain/Graph.java b/src/main/java/wooteco/subway/admin/domain/Graph.java new file mode 100644 index 000000000..05800a211 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/domain/Graph.java @@ -0,0 +1,49 @@ +package wooteco.subway.admin.domain; + +import java.util.List; +import java.util.Objects; + +import org.jgrapht.graph.WeightedMultigraph; + +import wooteco.subway.admin.exception.NotFoundLineException; + +public class Graph { + + private final WeightedMultigraph graph; + + private Graph(WeightedMultigraph graph) { + this.graph = graph; + } + + public static Graph of(WeightedMultigraph graph) { + return new Graph(graph); + } + + public static Graph of(List lines, PathType pathType) { + return new Graph(mapLinesToGraph(lines, pathType)); + } + + private static WeightedMultigraph mapLinesToGraph(List lines, + PathType pathType) { + if (Objects.isNull(lines)) { + throw new NotFoundLineException(); + } + WeightedMultigraph graph = new WeightedMultigraph<>( + LineStationEdge.class); + lines.stream() + .flatMap(it -> it.getLineStationsId().stream()) + .forEach(graph::addVertex); + + lines.stream() + .flatMap(it -> it.getStations().stream()) + .filter(it -> Objects.nonNull(it.getPreStationId())) + .forEach( + it -> graph.addEdge(it.getPreStationId(), it.getStationId(), + new LineStationEdge(it, pathType))); + return graph; + } + + public WeightedMultigraph getGraph() { + return graph; + } +} \ No newline at end of file diff --git a/src/main/java/wooteco/subway/admin/domain/Line.java b/src/main/java/wooteco/subway/admin/domain/Line.java index 5c573b745..a12fa5fbd 100644 --- a/src/main/java/wooteco/subway/admin/domain/Line.java +++ b/src/main/java/wooteco/subway/admin/domain/Line.java @@ -1,36 +1,51 @@ package wooteco.subway.admin.domain; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + import org.springframework.data.annotation.Id; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.*; +public class Line extends TimeEntity { -public class Line { @Id private Long id; private String name; + private String backgroundColor; private LocalTime startTime; private LocalTime endTime; private int intervalTime; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - private Set stations = new HashSet<>(); + private Set stations; public Line() { } - public Line(Long id, String name, LocalTime startTime, LocalTime endTime, int intervalTime) { + public Line(Long id, String name, String backgroundColor, LocalTime startTime, + LocalTime endTime, int intervalTime, Set stations) { + this.id = id; this.name = name; + this.backgroundColor = backgroundColor; this.startTime = startTime; this.endTime = endTime; this.intervalTime = intervalTime; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.stations = stations; + } + + public static Line of(Long id, String name, String backgroundColor, LocalTime startTime, + LocalTime endTime, int intervalTime) { + return new Line(id, name, backgroundColor, startTime, endTime, intervalTime, + new HashSet<>()); } - public Line(String name, LocalTime startTime, LocalTime endTime, int intervalTime) { - this(null, name, startTime, endTime, intervalTime); + public static Line of(String name, String backgroundColor, LocalTime startTime, + LocalTime endTime, + int intervalTime) { + return new Line(null, name, backgroundColor, startTime, endTime, intervalTime, + new HashSet<>()); } public Long getId() { @@ -49,6 +64,10 @@ public LocalTime getEndTime() { return endTime; } + public String getBackgroundColor() { + return backgroundColor; + } + public int getIntervalTime() { return intervalTime; } @@ -57,18 +76,15 @@ public Set getStations() { return stations; } - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - public void update(Line line) { if (line.getName() != null) { this.name = line.getName(); } + + if (line.getBackgroundColor() != null) { + this.backgroundColor = line.getBackgroundColor(); + } + if (line.getStartTime() != null) { this.startTime = line.getStartTime(); } @@ -78,29 +94,27 @@ public void update(Line line) { if (line.getIntervalTime() != 0) { this.intervalTime = line.getIntervalTime(); } - - this.updatedAt = LocalDateTime.now(); } public void addLineStation(LineStation lineStation) { stations.stream() - .filter(it -> Objects.equals(it.getPreStationId(), lineStation.getPreStationId())) - .findAny() - .ifPresent(it -> it.updatePreLineStation(lineStation.getStationId())); + .filter(it -> Objects.equals(it.getPreStationId(), lineStation.getPreStationId())) + .findAny() + .ifPresent(it -> it.updatePreLineStation(lineStation.getStationId())); stations.add(lineStation); } public void removeLineStationById(Long stationId) { LineStation targetLineStation = stations.stream() - .filter(it -> Objects.equals(it.getStationId(), stationId)) - .findFirst() - .orElseThrow(RuntimeException::new); + .filter(it -> Objects.equals(it.getStationId(), stationId)) + .findFirst() + .orElseThrow(RuntimeException::new); stations.stream() - .filter(it -> Objects.equals(it.getPreStationId(), stationId)) - .findFirst() - .ifPresent(it -> it.updatePreLineStation(targetLineStation.getPreStationId())); + .filter(it -> Objects.equals(it.getPreStationId(), stationId)) + .findFirst() + .ifPresent(it -> it.updatePreLineStation(targetLineStation.getPreStationId())); stations.remove(targetLineStation); } @@ -111,9 +125,9 @@ public List getLineStationsId() { } LineStation firstLineStation = stations.stream() - .filter(it -> it.getPreStationId() == null) - .findFirst() - .orElseThrow(RuntimeException::new); + .filter(it -> it.getPreStationId() == null) + .findFirst() + .orElseThrow(RuntimeException::new); List stationIds = new ArrayList<>(); stationIds.add(firstLineStation.getStationId()); @@ -121,8 +135,8 @@ public List getLineStationsId() { while (true) { Long lastStationId = stationIds.get(stationIds.size() - 1); Optional nextLineStation = stations.stream() - .filter(it -> Objects.equals(it.getPreStationId(), lastStationId)) - .findFirst(); + .filter(it -> Objects.equals(it.getPreStationId(), lastStationId)) + .findFirst(); if (!nextLineStation.isPresent()) { break; diff --git a/src/main/java/wooteco/subway/admin/domain/LineStation.java b/src/main/java/wooteco/subway/admin/domain/LineStation.java index 4e284ff3e..3b960ee51 100644 --- a/src/main/java/wooteco/subway/admin/domain/LineStation.java +++ b/src/main/java/wooteco/subway/admin/domain/LineStation.java @@ -1,10 +1,10 @@ package wooteco.subway.admin.domain; -public class LineStation { +public class LineStation extends TimeEntity { private Long preStationId; - private Long stationId; - private int distance; - private int duration; + private final Long stationId; + private final int distance; + private final int duration; public LineStation(Long preStationId, Long stationId, int distance, int duration) { this.preStationId = preStationId; @@ -32,4 +32,14 @@ public int getDuration() { public void updatePreLineStation(Long preStationId) { this.preStationId = preStationId; } + + @Override + public String toString() { + return "LineStation{" + + "preStationId=" + preStationId + + ", stationId=" + stationId + + ", distance=" + distance + + ", duration=" + duration + + '}'; + } } diff --git a/src/main/java/wooteco/subway/admin/domain/LineStationEdge.java b/src/main/java/wooteco/subway/admin/domain/LineStationEdge.java new file mode 100644 index 000000000..2ea4ed6e8 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/domain/LineStationEdge.java @@ -0,0 +1,35 @@ +package wooteco.subway.admin.domain; + +import org.jgrapht.graph.DefaultWeightedEdge; + +public class LineStationEdge extends DefaultWeightedEdge { + + private final LineStation lineStation; + private final PathType pathType; + + public LineStationEdge(LineStation lineStation, PathType pathType) { + this.lineStation = lineStation; + this.pathType = pathType; + } + + @Override + protected double getWeight() { + return pathType.getWeight(lineStation); + } + + public int getDistance() { + return lineStation.getDistance(); + } + + public int getDuration() { + return lineStation.getDuration(); + } + + public LineStation getLineStation() { + return lineStation; + } + + public PathType getPathType() { + return pathType; + } +} diff --git a/src/main/java/wooteco/subway/admin/domain/PathAlgorithm.java b/src/main/java/wooteco/subway/admin/domain/PathAlgorithm.java new file mode 100644 index 000000000..3b17563e1 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/domain/PathAlgorithm.java @@ -0,0 +1,6 @@ +package wooteco.subway.admin.domain; + +public interface PathAlgorithm { + + PathResult findPath(Long sourceId, Long targetId, Graph graph); +} diff --git a/src/main/java/wooteco/subway/admin/domain/PathAlgorithmByDijkstra.java b/src/main/java/wooteco/subway/admin/domain/PathAlgorithmByDijkstra.java new file mode 100644 index 000000000..88c3539bb --- /dev/null +++ b/src/main/java/wooteco/subway/admin/domain/PathAlgorithmByDijkstra.java @@ -0,0 +1,46 @@ +package wooteco.subway.admin.domain; + +import java.util.List; +import java.util.Objects; + +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.springframework.stereotype.Component; + +import wooteco.subway.admin.exception.IllegalStationNameException; +import wooteco.subway.admin.exception.NotFoundPathException; + +@Component +public class PathAlgorithmByDijkstra implements PathAlgorithm { + + public PathResult findPath(Long sourceId, Long targetId, Graph graph) { + DijkstraShortestPath dijkstraShortestPath = + new DijkstraShortestPath<>(graph.getGraph()); + validate(sourceId, targetId); + + if (Objects.isNull(dijkstraShortestPath.getPath(sourceId, targetId))) { + throw new NotFoundPathException(sourceId, targetId); + } + + return mapToPathResult(sourceId, targetId, dijkstraShortestPath); + } + + private void validate(Long sourceId, Long targetId) { + if (Objects.equals(sourceId, targetId)) { + throw new IllegalStationNameException(sourceId, targetId); + } + } + + private PathResult mapToPathResult(Long source, Long target, + DijkstraShortestPath dijkstraShortestPath) { + List path = dijkstraShortestPath.getPath(source, target).getVertexList(); + int totalDistance = 0; + int totalDuration = 0; + + for (LineStationEdge edge : dijkstraShortestPath.getPath(source, target).getEdgeList()) { + totalDistance += edge.getDistance(); + totalDuration += edge.getDuration(); + } + + return new PathResult(path, totalDistance, totalDuration); + } +} diff --git a/src/main/java/wooteco/subway/admin/domain/PathResult.java b/src/main/java/wooteco/subway/admin/domain/PathResult.java new file mode 100644 index 000000000..4718a46ca --- /dev/null +++ b/src/main/java/wooteco/subway/admin/domain/PathResult.java @@ -0,0 +1,27 @@ +package wooteco.subway.admin.domain; + +import java.util.List; + +public class PathResult { + private final List path; + private final int totalDistance; + private final int totalDuration; + + public PathResult(List path, int totalDistance, int totalDuration) { + this.path = path; + this.totalDistance = totalDistance; + this.totalDuration = totalDuration; + } + + public List getPath() { + return path; + } + + public int getTotalDistance() { + return totalDistance; + } + + public int getTotalDuration() { + return totalDuration; + } +} diff --git a/src/main/java/wooteco/subway/admin/domain/PathType.java b/src/main/java/wooteco/subway/admin/domain/PathType.java new file mode 100644 index 000000000..4e7673d77 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/domain/PathType.java @@ -0,0 +1,30 @@ +package wooteco.subway.admin.domain; + +import java.util.Objects; +import java.util.function.Function; + +import wooteco.subway.admin.exception.IllegalTypeNameException; + +public enum PathType { + DISTANCE(LineStation::getDistance), + DURATION(LineStation::getDuration); + + private final Function function; + + PathType(Function function) { + this.function = function; + } + + public static PathType of(String typeName) { + String upperCaseName = typeName.toUpperCase(); + if (!Objects.equals(upperCaseName, DURATION.name()) && !Objects.equals(upperCaseName, + DISTANCE.name())) { + throw new IllegalTypeNameException(typeName); + } + return valueOf(upperCaseName); + } + + public int getWeight(LineStation lineStation) { + return function.apply(lineStation); + } +} diff --git a/src/main/java/wooteco/subway/admin/domain/Station.java b/src/main/java/wooteco/subway/admin/domain/Station.java index d69630f5a..d65da44a7 100644 --- a/src/main/java/wooteco/subway/admin/domain/Station.java +++ b/src/main/java/wooteco/subway/admin/domain/Station.java @@ -2,26 +2,22 @@ import org.springframework.data.annotation.Id; -import java.time.LocalDateTime; +public class Station extends TimeEntity { -public class Station { @Id private Long id; private String name; - private LocalDateTime createdAt; public Station() { } public Station(String name) { this.name = name; - this.createdAt = LocalDateTime.now(); } public Station(Long id, String name) { this.id = id; this.name = name; - this.createdAt = LocalDateTime.now(); } public Long getId() { @@ -31,8 +27,4 @@ public Long getId() { public String getName() { return name; } - - public LocalDateTime getCreatedAt() { - return createdAt; - } } diff --git a/src/main/java/wooteco/subway/admin/domain/TimeEntity.java b/src/main/java/wooteco/subway/admin/domain/TimeEntity.java new file mode 100644 index 000000000..76d151c43 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/domain/TimeEntity.java @@ -0,0 +1,39 @@ +package wooteco.subway.admin.domain; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; + +public abstract class TimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + public TimeEntity() { + } + + public TimeEntity(LocalDateTime createdAt, LocalDateTime updatedAt) { + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public void create(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public void update(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/wooteco/subway/admin/dto/ErrorResponse.java b/src/main/java/wooteco/subway/admin/dto/ErrorResponse.java new file mode 100644 index 000000000..754c3c249 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/dto/ErrorResponse.java @@ -0,0 +1,17 @@ +package wooteco.subway.admin.dto; + +public class ErrorResponse { + + private String errorMessage; + + public ErrorResponse() { + } + + public ErrorResponse(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/src/main/java/wooteco/subway/admin/dto/LineDetailResponse.java b/src/main/java/wooteco/subway/admin/dto/LineDetailResponse.java index bbea0ef51..08068efff 100644 --- a/src/main/java/wooteco/subway/admin/dto/LineDetailResponse.java +++ b/src/main/java/wooteco/subway/admin/dto/LineDetailResponse.java @@ -1,15 +1,16 @@ package wooteco.subway.admin.dto; -import wooteco.subway.admin.domain.Line; -import wooteco.subway.admin.domain.Station; - import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; +import wooteco.subway.admin.domain.Line; +import wooteco.subway.admin.domain.Station; + public class LineDetailResponse { private Long id; private String name; + private String backgroundColor; private LocalTime startTime; private LocalTime endTime; private int intervalTime; @@ -20,9 +21,12 @@ public class LineDetailResponse { public LineDetailResponse() { } - public LineDetailResponse(Long id, String name, LocalTime startTime, LocalTime endTime, int intervalTime, LocalDateTime createdAt, LocalDateTime updatedAt, List stations) { + public LineDetailResponse(Long id, String name, String backgroundColor, + LocalTime startTime, LocalTime endTime, int intervalTime, LocalDateTime createdAt, + LocalDateTime updatedAt, List stations) { this.id = id; this.name = name; + this.backgroundColor = backgroundColor; this.startTime = startTime; this.endTime = endTime; this.intervalTime = intervalTime; @@ -32,7 +36,9 @@ public LineDetailResponse(Long id, String name, LocalTime startTime, LocalTime e } public static LineDetailResponse of(Line line, List stations) { - return new LineDetailResponse(line.getId(), line.getName(), line.getStartTime(), line.getEndTime(), line.getIntervalTime(), line.getCreatedAt(), line.getUpdatedAt(), stations); + return new LineDetailResponse(line.getId(), line.getName(), line.getBackgroundColor(), + line.getStartTime(), line.getEndTime(), line.getIntervalTime(), line.getCreatedAt(), + line.getUpdatedAt(), stations); } public Long getId() { @@ -43,6 +49,10 @@ public String getName() { return name; } + public String getBackgroundColor() { + return backgroundColor; + } + public LocalTime getStartTime() { return startTime; } diff --git a/src/main/java/wooteco/subway/admin/dto/LineRequest.java b/src/main/java/wooteco/subway/admin/dto/LineRequest.java index a32ad26dd..2baa18a6a 100644 --- a/src/main/java/wooteco/subway/admin/dto/LineRequest.java +++ b/src/main/java/wooteco/subway/admin/dto/LineRequest.java @@ -1,13 +1,28 @@ package wooteco.subway.admin.dto; -import wooteco.subway.admin.domain.Line; - import java.time.LocalTime; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import wooteco.subway.admin.domain.Line; + public class LineRequest { + + @NotBlank(message = "노선 이름은 필수 입력 요소입니다.") private String name; + + @NotBlank(message = "노선 색상은 필수 입력 요소입니다.") + private String backgroundColor; + + @NotNull(message = "첫차 시간은 필수 입력 요소입니다.") private LocalTime startTime; + + @NotNull(message = "막차 시간은 필수 입력 요소입니다.") private LocalTime endTime; + + @Min(value = 1, message = "간격은 1 이상만 입력 가능합니다.") private int intervalTime; public LineRequest() { @@ -17,6 +32,10 @@ public String getName() { return name; } + public String getBackgroundColor() { + return backgroundColor; + } + public LocalTime getStartTime() { return startTime; } @@ -30,6 +49,6 @@ public int getIntervalTime() { } public Line toLine() { - return new Line(name, startTime, endTime, intervalTime); + return Line.of(name, backgroundColor, startTime, endTime, intervalTime); } } diff --git a/src/main/java/wooteco/subway/admin/dto/LineResponse.java b/src/main/java/wooteco/subway/admin/dto/LineResponse.java index 7aebc4d9e..bd2b45f2d 100644 --- a/src/main/java/wooteco/subway/admin/dto/LineResponse.java +++ b/src/main/java/wooteco/subway/admin/dto/LineResponse.java @@ -10,6 +10,7 @@ public class LineResponse { private Long id; private String name; + private String backgroundColor; private LocalTime startTime; private LocalTime endTime; private int intervalTime; @@ -17,21 +18,22 @@ public class LineResponse { private LocalDateTime updatedAt; public static LineResponse of(Line line) { - return new LineResponse(line.getId(), line.getName(), line.getStartTime(), line.getEndTime(), line.getIntervalTime(), line.getCreatedAt(), line.getUpdatedAt()); + return new LineResponse(line.getId(), line.getName(), line.getBackgroundColor(), line.getStartTime(), line.getEndTime(), line.getIntervalTime(), line.getCreatedAt(), line.getUpdatedAt()); } public static List listOf(List lines) { return lines.stream() - .map(it -> LineResponse.of(it)) + .map(LineResponse::of) .collect(Collectors.toList()); } public LineResponse() { } - public LineResponse(Long id, String name, LocalTime startTime, LocalTime endTime, int intervalTime, LocalDateTime createdAt, LocalDateTime updatedAt) { + public LineResponse(Long id, String name, String backgroundColor, LocalTime startTime, LocalTime endTime, int intervalTime, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.name = name; + this.backgroundColor = backgroundColor; this.startTime = startTime; this.endTime = endTime; this.intervalTime = intervalTime; @@ -59,6 +61,10 @@ public int getIntervalTime() { return intervalTime; } + public String getBackgroundColor() { + return backgroundColor; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/wooteco/subway/admin/dto/PathRequest.java b/src/main/java/wooteco/subway/admin/dto/PathRequest.java new file mode 100644 index 000000000..3fffd13bc --- /dev/null +++ b/src/main/java/wooteco/subway/admin/dto/PathRequest.java @@ -0,0 +1,35 @@ +package wooteco.subway.admin.dto; + +import javax.validation.constraints.NotBlank; + +public class PathRequest { + + @NotBlank(message = "이전 역 이름은 필수 입력 요소입니다.") + private String sourceName; + @NotBlank(message = "대상 역 이름은 필수 입력 요소입니다.") + private String targetName; + private String type; + + public PathRequest() { + } + + public PathRequest( + @NotBlank(message = "이전 역 이름은 필수 입력 요소입니다.") String sourceName, + @NotBlank(message = "대상 역 이름은 필수 입력 요소입니다.") String targetName, String type) { + this.sourceName = sourceName; + this.targetName = targetName; + this.type = type; + } + + public String getSourceName() { + return sourceName; + } + + public String getTargetName() { + return targetName; + } + + public String getType() { + return type; + } +} diff --git a/src/main/java/wooteco/subway/admin/dto/PathResponse.java b/src/main/java/wooteco/subway/admin/dto/PathResponse.java new file mode 100644 index 000000000..bc1c971d0 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/dto/PathResponse.java @@ -0,0 +1,31 @@ +package wooteco.subway.admin.dto; + +import java.util.List; + +public class PathResponse { + private List stations; + private int totalDistance; + private int totalDuration; + + public PathResponse() { + } + + public PathResponse(List stations, int totalDistance, + int totalDuration) { + this.stations = stations; + this.totalDistance = totalDistance; + this.totalDuration = totalDuration; + } + + public List getStations() { + return stations; + } + + public int getTotalDuration() { + return totalDuration; + } + + public int getTotalDistance() { + return totalDistance; + } +} diff --git a/src/main/java/wooteco/subway/admin/dto/StationCreateRequest.java b/src/main/java/wooteco/subway/admin/dto/StationCreateRequest.java index 08f017a71..a3958f9cc 100644 --- a/src/main/java/wooteco/subway/admin/dto/StationCreateRequest.java +++ b/src/main/java/wooteco/subway/admin/dto/StationCreateRequest.java @@ -1,11 +1,21 @@ package wooteco.subway.admin.dto; +import javax.validation.constraints.NotBlank; import wooteco.subway.admin.domain.Station; public class StationCreateRequest { + + @NotBlank(message = "이름은 필수 입력 항목입니다.") private String name; + public StationCreateRequest() { + } + + public StationCreateRequest(String name) { + this.name = name; + } + public String getName() { return name; } diff --git a/src/main/java/wooteco/subway/admin/dto/StationResponse.java b/src/main/java/wooteco/subway/admin/dto/StationResponse.java index b8e1ffc88..48d4804fd 100644 --- a/src/main/java/wooteco/subway/admin/dto/StationResponse.java +++ b/src/main/java/wooteco/subway/admin/dto/StationResponse.java @@ -1,11 +1,11 @@ package wooteco.subway.admin.dto; -import wooteco.subway.admin.domain.Station; - import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; +import wooteco.subway.admin.domain.Station; + public class StationResponse { private Long id; private String name; @@ -17,8 +17,8 @@ public static StationResponse of(Station station) { public static List listOf(List stations) { return stations.stream() - .map(StationResponse::of) - .collect(Collectors.toList()); + .map(StationResponse::of) + .collect(Collectors.toList()); } public StationResponse() { diff --git a/src/main/java/wooteco/subway/admin/dto/WholeSubwayResponse.java b/src/main/java/wooteco/subway/admin/dto/WholeSubwayResponse.java index 4c140a4bf..95424aa43 100644 --- a/src/main/java/wooteco/subway/admin/dto/WholeSubwayResponse.java +++ b/src/main/java/wooteco/subway/admin/dto/WholeSubwayResponse.java @@ -2,9 +2,21 @@ import java.util.List; -// TODO 구현하세요 :) public class WholeSubwayResponse { - public static WholeSubwayResponse of(List responses) { - return null; + private List lineDetailResponse; + + public static WholeSubwayResponse of(List lineDetailResponses) { + return new WholeSubwayResponse(lineDetailResponses); + } + + public WholeSubwayResponse() { + } + + public WholeSubwayResponse(List lineDetailResponse) { + this.lineDetailResponse = lineDetailResponse; + } + + public List getLineDetailResponse() { + return lineDetailResponse; } } diff --git a/src/main/java/wooteco/subway/admin/exception/BusinessException.java b/src/main/java/wooteco/subway/admin/exception/BusinessException.java new file mode 100644 index 000000000..39655836c --- /dev/null +++ b/src/main/java/wooteco/subway/admin/exception/BusinessException.java @@ -0,0 +1,7 @@ +package wooteco.subway.admin.exception; + +public class BusinessException extends RuntimeException { + public BusinessException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/admin/exception/IllegalStationNameException.java b/src/main/java/wooteco/subway/admin/exception/IllegalStationNameException.java new file mode 100644 index 000000000..870651ff5 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/exception/IllegalStationNameException.java @@ -0,0 +1,8 @@ +package wooteco.subway.admin.exception; + +public class IllegalStationNameException extends BusinessException { + + public IllegalStationNameException(Long sourceId, Long targetId) { + super(sourceId + ", " + targetId + " 역이 중복되었습니다."); + } +} diff --git a/src/main/java/wooteco/subway/admin/exception/IllegalTypeNameException.java b/src/main/java/wooteco/subway/admin/exception/IllegalTypeNameException.java new file mode 100644 index 000000000..cd97663f2 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/exception/IllegalTypeNameException.java @@ -0,0 +1,8 @@ +package wooteco.subway.admin.exception; + +public class IllegalTypeNameException extends BusinessException { + + public IllegalTypeNameException(String typeName) { + super(typeName + "방식의 경로는 지원하지 않습니다."); + } +} diff --git a/src/main/java/wooteco/subway/admin/exception/NotFoundLineException.java b/src/main/java/wooteco/subway/admin/exception/NotFoundLineException.java new file mode 100644 index 000000000..52baf3c60 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/exception/NotFoundLineException.java @@ -0,0 +1,14 @@ +package wooteco.subway.admin.exception; + +import org.springframework.dao.DataAccessException; + +public class NotFoundLineException extends DataAccessException { + + public NotFoundLineException() { + super("line을 찾을 수 없습니다"); + } + + public NotFoundLineException(Long id) { + super(id + "에 해당하는 line을 찾을 수 없습니다"); + } +} diff --git a/src/main/java/wooteco/subway/admin/exception/NotFoundPathException.java b/src/main/java/wooteco/subway/admin/exception/NotFoundPathException.java new file mode 100644 index 000000000..dc7ab2042 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/exception/NotFoundPathException.java @@ -0,0 +1,8 @@ +package wooteco.subway.admin.exception; + +public class NotFoundPathException extends BusinessException { + + public NotFoundPathException(Long source, Long target) { + super(source + "에서 " + target + "으로 가는 경로가 존재하지 않습니다."); + } +} diff --git a/src/main/java/wooteco/subway/admin/exception/NotFoundStationException.java b/src/main/java/wooteco/subway/admin/exception/NotFoundStationException.java new file mode 100644 index 000000000..772107022 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/exception/NotFoundStationException.java @@ -0,0 +1,14 @@ +package wooteco.subway.admin.exception; + +import org.springframework.dao.DataAccessException; + +public class NotFoundStationException extends DataAccessException { + + public NotFoundStationException() { + super("해당역을 찾을 수 없습니다"); + } + + public NotFoundStationException(String name) { + super(name + "역을 찾을 수 없습니다."); + } +} diff --git a/src/main/java/wooteco/subway/admin/repository/LineRepository.java b/src/main/java/wooteco/subway/admin/repository/LineRepository.java index 83ddc5790..a1e7ceca0 100644 --- a/src/main/java/wooteco/subway/admin/repository/LineRepository.java +++ b/src/main/java/wooteco/subway/admin/repository/LineRepository.java @@ -1,9 +1,10 @@ package wooteco.subway.admin.repository; +import java.util.List; + import org.springframework.data.repository.CrudRepository; -import wooteco.subway.admin.domain.Line; -import java.util.List; +import wooteco.subway.admin.domain.Line; public interface LineRepository extends CrudRepository { @Override diff --git a/src/main/java/wooteco/subway/admin/repository/StationRepository.java b/src/main/java/wooteco/subway/admin/repository/StationRepository.java index 5cb66563f..d72ff4349 100644 --- a/src/main/java/wooteco/subway/admin/repository/StationRepository.java +++ b/src/main/java/wooteco/subway/admin/repository/StationRepository.java @@ -1,12 +1,13 @@ package wooteco.subway.admin.repository; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; -import wooteco.subway.admin.domain.Station; -import java.util.List; -import java.util.Optional; +import wooteco.subway.admin.domain.Station; public interface StationRepository extends CrudRepository { @Override diff --git a/src/main/java/wooteco/subway/admin/service/LineService.java b/src/main/java/wooteco/subway/admin/service/LineService.java index 88b7ab4ff..04790b15b 100644 --- a/src/main/java/wooteco/subway/admin/service/LineService.java +++ b/src/main/java/wooteco/subway/admin/service/LineService.java @@ -1,38 +1,70 @@ package wooteco.subway.admin.service; +import java.util.List; +import java.util.stream.Collectors; + import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import wooteco.subway.admin.domain.Line; import wooteco.subway.admin.domain.LineStation; import wooteco.subway.admin.domain.Station; import wooteco.subway.admin.dto.LineDetailResponse; import wooteco.subway.admin.dto.LineRequest; +import wooteco.subway.admin.dto.LineResponse; import wooteco.subway.admin.dto.LineStationCreateRequest; import wooteco.subway.admin.dto.WholeSubwayResponse; +import wooteco.subway.admin.exception.NotFoundLineException; import wooteco.subway.admin.repository.LineRepository; -import wooteco.subway.admin.repository.StationRepository; - -import java.util.List; @Service +@Transactional public class LineService { - private LineRepository lineRepository; - private StationRepository stationRepository; + private final LineRepository lineRepository; + private final StationService stationService; - public LineService(LineRepository lineRepository, StationRepository stationRepository) { + public LineService(LineRepository lineRepository, + StationService stationService) { this.lineRepository = lineRepository; - this.stationRepository = stationRepository; + this.stationService = stationService; } - public Line save(Line line) { - return lineRepository.save(line); + public LineResponse save(Line line) { + return LineResponse.of(lineRepository.save(line)); } - public List showLines() { - return lineRepository.findAll(); + @Transactional(readOnly = true) + public List findLines() { + return LineResponse.listOf(lineRepository.findAll()); + } + + @Transactional(readOnly = true) + public LineResponse findLine(Long id) { + Line line = lineRepository.findById(id).orElseThrow(() -> new NotFoundLineException(id)); + + return LineResponse.of(line); + } + + @Transactional(readOnly = true) + public LineDetailResponse findDetailLine(Long id) { + Line line = lineRepository.findById(id).orElseThrow(() -> new NotFoundLineException(id)); + List stations = stationService.findAllById(line.getLineStationsId()); + return LineDetailResponse.of(line, stations); + } + + @Transactional(readOnly = true) + public WholeSubwayResponse findDetailLines() { + List lines = lineRepository.findAll(); + List lineDetailResponses = lines.stream() + .map(line -> + LineDetailResponse.of(line, stationService.findAllById(line.getLineStationsId()))) + .collect(Collectors.toList()); + return WholeSubwayResponse.of(lineDetailResponses); } public void updateLine(Long id, LineRequest request) { - Line persistLine = lineRepository.findById(id).orElseThrow(RuntimeException::new); + Line persistLine = lineRepository.findById(id) + .orElseThrow(() -> new NotFoundLineException(id)); persistLine.update(request.toLine()); lineRepository.save(persistLine); } @@ -43,26 +75,22 @@ public void deleteLineById(Long id) { public void addLineStation(Long id, LineStationCreateRequest request) { Line line = lineRepository.findById(id).orElseThrow(RuntimeException::new); - LineStation lineStation = new LineStation(request.getPreStationId(), request.getStationId(), request.getDistance(), request.getDuration()); + LineStation lineStation = new LineStation(request.getPreStationId(), request.getStationId(), + request.getDistance(), request.getDuration()); line.addLineStation(lineStation); lineRepository.save(line); } public void removeLineStation(Long lineId, Long stationId) { - Line line = lineRepository.findById(lineId).orElseThrow(RuntimeException::new); + Line line = lineRepository.findById(lineId) + .orElseThrow(() -> new NotFoundLineException(lineId)); line.removeLineStationById(stationId); lineRepository.save(line); } - public LineDetailResponse findLineWithStationsById(Long id) { - Line line = lineRepository.findById(id).orElseThrow(RuntimeException::new); - List stations = stationRepository.findAllById(line.getLineStationsId()); - return LineDetailResponse.of(line, stations); - } - - // TODO: 구현하세요 :) - public WholeSubwayResponse wholeLines() { - return null; + @Transactional(readOnly = true) + public List findAll() { + return lineRepository.findAll(); } } diff --git a/src/main/java/wooteco/subway/admin/service/PathService.java b/src/main/java/wooteco/subway/admin/service/PathService.java new file mode 100644 index 000000000..384ff9da8 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/service/PathService.java @@ -0,0 +1,61 @@ +package wooteco.subway.admin.service; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import wooteco.subway.admin.domain.Graph; +import wooteco.subway.admin.domain.Line; +import wooteco.subway.admin.domain.PathAlgorithm; +import wooteco.subway.admin.domain.PathResult; +import wooteco.subway.admin.domain.PathType; +import wooteco.subway.admin.dto.PathRequest; +import wooteco.subway.admin.dto.PathResponse; +import wooteco.subway.admin.dto.StationResponse; +import wooteco.subway.admin.exception.NotFoundStationException; + +@Service +@Transactional(readOnly = true) +public class PathService { + + private final StationService stationService; + private final LineService lineService; + private final PathAlgorithm pathAlgorithm; + + public PathService(StationService stationService, + LineService lineService, PathAlgorithm pathAlgorithm) { + this.stationService = stationService; + this.lineService = lineService; + this.pathAlgorithm = pathAlgorithm; + } + + public PathResponse findPath(PathRequest request) { + Long sourceId = stationService.findIdByName(request.getSourceName()); + Long targetId = stationService.findIdByName(request.getTargetName()); + List lines = lineService.findAll(); + Graph graph = Graph.of(lines, PathType.of(request.getType())); + + PathResult pathResult = pathAlgorithm.findPath(sourceId, targetId, graph); + List path = pathResult.getPath(); + List stationResponses = StationResponse.listOf( + stationService.findAllById(path)); + int totalDistance = pathResult.getTotalDistance(); + int totalDuration = pathResult.getTotalDuration(); + List sortedStationResponses = sort(path, stationResponses); + + return new PathResponse(sortedStationResponses, totalDistance, totalDuration); + } + + private List sort(List path, List stationResponses) { + List result = new ArrayList<>(); + for (Long stationId : path) { + StationResponse response = stationResponses.stream() + .filter(stationResponse -> stationResponse.getId().equals(stationId)) + .findAny().orElseThrow(NotFoundStationException::new); + result.add(response); + } + return result; + } +} diff --git a/src/main/java/wooteco/subway/admin/service/StationService.java b/src/main/java/wooteco/subway/admin/service/StationService.java new file mode 100644 index 000000000..79f493062 --- /dev/null +++ b/src/main/java/wooteco/subway/admin/service/StationService.java @@ -0,0 +1,51 @@ +package wooteco.subway.admin.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import wooteco.subway.admin.domain.Station; +import wooteco.subway.admin.dto.StationCreateRequest; +import wooteco.subway.admin.dto.StationResponse; +import wooteco.subway.admin.exception.NotFoundStationException; +import wooteco.subway.admin.repository.StationRepository; + +@Service +@Transactional(readOnly = true) +public class StationService { + + private final StationRepository stationRepository; + + public StationService(StationRepository stationRepository) { + this.stationRepository = stationRepository; + } + + @Transactional + public StationResponse save(StationCreateRequest request) { + Station persistStation = stationRepository.save(request.toStation()); + + return StationResponse.of(persistStation); + } + + public List findAll() { + List stations = stationRepository.findAll(); + + return StationResponse.listOf(stations); + } + + + public List findAllById(List lineStationsId) { + return stationRepository.findAllById(lineStationsId); + } + + public Long findIdByName(String name) { + return stationRepository.findByName(name) + .orElseThrow(() -> new NotFoundStationException(name)).getId(); + } + + @Transactional + public void deleteById(Long id) { + stationRepository.deleteById(id); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cda10b327..74637bbf7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,4 @@ logging.level.org.springframework.jdbc.core.JdbcTemplate=debug spring.h2.console.enabled=true -handlebars.suffix=.html \ No newline at end of file +handlebars.suffix=.html +spring.datasource.data=classpath:data.sql \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..3464edffc --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,28 @@ +insert into station (name) +VALUES ('잠실'), + ('잠실새내'), + ('종합운동장'), + ('삼전'), + ('석촌고분'), + ('석촌'), + ('부산'), + ('대구'); + +insert into line (name, background_color, start_time, end_time, interval_time) +VALUES ('2호선', 'bg-green-400', current_time, current_time, 3), + ('9호선', 'bg-yellow-500', current_time, current_time, 3), + ('8호선', 'bg-pink-600', current_time, current_time, 3), + ('ktx', 'bg-indigo-600', current_time, current_time, 3); + +insert into line_station (line, station_id, pre_station_id, distance, duration) +VALUES (1, 1, null, 0, 0), + (1, 2, 1, 10, 1), + (1, 3, 2, 10, 1), + (2, 3, null, 0, 0), + (2, 4, 3, 10, 1), + (2, 5, 4, 1, 10), + (2, 6, 5, 1, 10), + (3, 1, null, 0, 0), + (3, 6, 1, 1, 10), + (4, 7, null, 10, 10), + (4, 8, 7, 10, 10); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 5f8edfbe8..cdc1973ba 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,30 +1,32 @@ create table if not exists STATION ( - id bigint auto_increment not null, - name varchar(255) not null unique, - created_at datetime, - primary key(id) + id bigint auto_increment not null, + name varchar(255) unique not null, + created_at datetime, + updated_at datetime, + primary key (id) ); create table if not exists LINE ( - id bigint auto_increment not null, - name varchar(255) not null, - start_time time not null, - end_time time not null, - interval_time int not null, - created_at datetime, - updated_at datetime, - primary key(id) + id bigint auto_increment not null, + name varchar(255) unique not null, + background_color varchar(255) not null, + start_time time not null, + end_time time not null, + interval_time int not null, + created_at datetime, + updated_at datetime, + primary key (id) ); create table if not exists LINE_STATION ( - line bigint not null, - station_id bigint not null, + line bigint not null, + station_id bigint not null, pre_station_id bigint, - distance int, - duration int, - created_at datetime, - updated_at datetime + distance int, + duration int, + created_at datetime, + updated_at datetime ); \ No newline at end of file diff --git a/src/main/resources/static/admin/js/views/AdminEdge.js b/src/main/resources/static/admin/js/views/AdminEdge.js index 4f1aa5b70..7db1da408 100644 --- a/src/main/resources/static/admin/js/views/AdminEdge.js +++ b/src/main/resources/static/admin/js/views/AdminEdge.js @@ -1,52 +1,169 @@ -import { - optionTemplate, - subwayLinesItemTemplate -} from "../../utils/templates.js"; -import { defaultSubwayLines } from "../../utils/subwayMockData.js"; +import { optionTemplate, subwayLinesItemTemplate, } from "../../utils/templates.js"; import tns from "../../lib/slider/tiny-slider.js"; -import { EVENT_TYPE } from "../../utils/constants.js"; +import { EVENT_TYPE, KEY_TYPE } from "../../utils/constants.js"; import Modal from "../../ui/Modal.js"; function AdminEdge() { const $subwayLinesSlider = document.querySelector(".subway-lines-slider"); const createSubwayEdgeModal = new Modal(); + const $createSubmitButton = document.querySelector("#submit-button"); const initSubwayLinesSlider = () => { - $subwayLinesSlider.innerHTML = defaultSubwayLines + let statusCode; + + fetch('/lines/detail', { + method: 'GET', + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }).then(jsonResponse => { + $subwayLinesSlider.innerHTML = jsonResponse.lineDetailResponse .map(line => subwayLinesItemTemplate(line)) .join(""); - tns({ - container: ".subway-lines-slider", - loop: true, - slideBy: "page", - speed: 400, - autoplayButtonOutput: false, - mouseDrag: true, - lazyload: true, - controlsContainer: "#slider-controls", - items: 1, - edgePadding: 25 - }); + tns({ + container: ".subway-lines-slider", + loop: true, + slideBy: "page", + speed: 400, + autoplayButtonOutput: false, + mouseDrag: true, + lazyLoad: true, + controlsContainer: "#slider-controls", + items: 1, + edgePadding: 25 + }); + }).catch(response => response.json().then(error => alert(error.message))); }; const initSubwayLineOptions = () => { - const subwayLineOptionTemplate = defaultSubwayLines - .map(line => optionTemplate(line.title)) + + fetch('/lines', { + method: 'GET', + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then(jsonResponse => { + const subwayLineOptionTemplate = jsonResponse + .map(line => optionTemplate(line)) .join(""); - const $stationSelectOptions = document.querySelector( - "#station-select-options" - ); - $stationSelectOptions.insertAdjacentHTML( - "afterbegin", - subwayLineOptionTemplate - ); + const $lineSelectOptions = document.querySelector( + "#line-select-options" + ); + $lineSelectOptions.insertAdjacentHTML( + "afterbegin", + subwayLineOptionTemplate + ); + }).catch(response => response.json().then(error => alert(error.errorMessage))); }; + const onCreateStationHandler = event => { + if (event.key !== KEY_TYPE.ENTER && event.type !== EVENT_TYPE.CLICK) { + return; + } + let statusCode; + const selectLines = document.querySelector("#line-select-options"); + const selectPreStation = document.querySelector("#pre-station-select-options"); + const selectStation = document.querySelector("#station-select-options"); + + const data = { + lineId: selectLines.options[selectLines.selectedIndex].value, + preStationId: selectPreStation.value.trim(), + stationId: selectStation.value.trim(), + distance: document.querySelector("#distance").value, + duration: document.querySelector("#duration").value + } + + validate(data); + + fetch('/lines/' + data.lineId + '/stations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }).then(jsonResponse => async function (jsonResponse) { + if (statusCode !== 500) { + const response = await fetch('/lines/detail', { + method: 'GET' + }); + const jsonResponse = await response.json(); + $subwayLinesSlider.innerHTML = jsonResponse + .map(line => subwayLinesItemTemplate(line)) + .join(""); + tns({ + container: ".subway-lines-slider", + loop: true, + slideBy: "page", + speed: 400, + autoplayButtonOutput: false, + mouseDrag: true, + lazyLoad: true, + controlsContainer: "#slider-controls", + items: 1, + edgePadding: 25 + }); + } + }).catch(response => response.json().then(error => alert(error.errorMessage))); + } + + function validate(data) { + const lineId = data.lineId; + const list = document.querySelectorAll(".list-item"); + const array = Array.from(list); + + if (duplicatedName(lineId, array, data)) { + alert("추가하려는 역이 이미 존재합니다."); + throw new Error(); + } + + if (notExistingPreStationName(lineId, array, data)) { + alert("이전 역이 적절하지 않습니다."); + throw new Error(); + } + } + + function duplicatedName(lineId, array, data) { + return array.some(element => { + return Number(element.dataset.lineId) === Number(lineId) && Number(element.dataset.stationId) === Number( + data.stationId); + }); + } + + function notExistingPreStationName(lineId, array, data) { + if (data.preStationId === "") { + return false; + } + for (const element of array) { + if (Number(element.dataset.lineId) === Number(lineId) && Number(element.dataset.stationId) === Number( + data.preStationId)) { + return false; + } + } + return array.length !== 0; + + } + const onRemoveStationHandler = event => { const $target = event.target; const isDeleteButton = $target.classList.contains("mdi-delete"); if (isDeleteButton) { $target.closest(".list-item").remove(); + + const lineId = $target.closest(".list-item").dataset.lineId; + const stationId = $target.closest(".list-item").dataset.stationId; + fetch("/lines/" + lineId + "/stations/" + stationId, { + method: 'DELETE' + }).catch(alert); } }; @@ -55,12 +172,73 @@ function AdminEdge() { EVENT_TYPE.CLICK, onRemoveStationHandler ); + $createSubmitButton.addEventListener( + EVENT_TYPE.CLICK, + onCreateStationHandler + ); }; + function initSubwayStationOptions() { + fetch('/stations', { + method: 'GET', + }).then(response => response.json()) + .then(stations => { + const stationsTemplate = stations.map(optionTemplate).join(''); + + const $preStationSelectOptions = document.querySelector( + "#pre-station-select-options" + ); + const $stationSelectOptions = document.querySelector( + "#station-select-options" + ); + $stationSelectOptions.insertAdjacentHTML( + "afterbegin", + stationsTemplate + ); + + $preStationSelectOptions.insertAdjacentHTML( + "afterbegin", + '' + ); + + $preStationSelectOptions.insertAdjacentHTML( + "afterbegin", + stationsTemplate + ); + }) + } + this.init = () => { initSubwayLinesSlider(); initSubwayLineOptions(); + initSubwayStationOptions(); initEventListeners(); + + fetch('/lines/detail', { + method: 'GET' + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json() + }) + .then(lines => { + $subwayLinesSlider.innerHTML = lines.lineDetailResponse + .map(lineDetailResponse => subwayLinesItemTemplate(lineDetailResponse)) + .join(""); + tns({ + container: ".subway-lines-slider", + loop: true, + slideBy: "page", + speed: 400, + autoplayButtonOutput: false, + mouseDrag: true, + lazyLoad: true, + controlsContainer: "#slider-controls", + items: 1, + edgePadding: 25 + }); + }).catch(response => response.json().then(error => alert(error.errorMessage))); }; } diff --git a/src/main/resources/static/admin/js/views/AdminLine.js b/src/main/resources/static/admin/js/views/AdminLine.js index 203e2892b..80bd65433 100644 --- a/src/main/resources/static/admin/js/views/AdminLine.js +++ b/src/main/resources/static/admin/js/views/AdminLine.js @@ -1,50 +1,188 @@ -import { EVENT_TYPE } from "../../utils/constants.js"; +import { ERROR_MESSAGE, EVENT_TYPE } from "../../utils/constants.js"; import { - subwayLinesTemplate, - colorSelectOptionTemplate + colorSelectOptionTemplate, + innerSubwayLinesTemplate, + subwayLinesTemplate } from "../../utils/templates.js"; -import { defaultSubwayLines } from "../../utils/subwayMockData.js"; import { subwayLineColorOptions } from "../../utils/defaultSubwayData.js"; import Modal from "../../ui/Modal.js"; + function AdminLine() { const $subwayLineList = document.querySelector("#subway-line-list"); const $subwayLineNameInput = document.querySelector("#subway-line-name"); + const $subwayLineFirstTimeInput = document.querySelector("#first-time"); + const $subwayLineLastTimeInput = document.querySelector("#last-time"); + const $subwayLineIntervalTimeInput = document.querySelector("#interval-time"); const $subwayLineColorInput = document.querySelector("#subway-line-color"); + let updateId = null; const $createSubwayLineButton = document.querySelector( "#subway-line-create-form #submit-button" ); const subwayLineModal = new Modal(); + function updateLine(data) { + fetch("/lines/" + updateId, { + method: "PUT", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }).then(response => { + if (!response.ok) { + throw response + } + return response.json(); + }).then(jsonResponse => { + let lines = document.querySelectorAll(".line-id"); + for (let line of lines) { + if (line.innerText.trim() === updateId) { + line.parentNode.innerHTML = innerSubwayLinesTemplate(jsonResponse); + } + } + updateId = null; + }).catch(error => error.json()).then(json => alert(json.errorMessage)); + } + + function createLine(data) { + fetch("/lines", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then(jsonResponse => { + const newSubwayLine = { + id: jsonResponse.id, + name: jsonResponse.name, + backgroundColor: jsonResponse.backgroundColor + }; + $subwayLineList.insertAdjacentHTML( + "beforeend", + subwayLinesTemplate(newSubwayLine) + ); + }).catch(error => error.json()).then(error => alert(error.errorMessage)); + } + const onCreateSubwayLine = event => { event.preventDefault(); - const newSubwayLine = { - title: $subwayLineNameInput.value, - bgColor: $subwayLineColorInput.value - }; - $subwayLineList.insertAdjacentHTML( - "beforeend", - subwayLinesTemplate(newSubwayLine) - ); + + const data = { + name: $subwayLineNameInput.value, + startTime: $subwayLineFirstTimeInput.value, + endTime: $subwayLineLastTimeInput.value, + intervalTime: $subwayLineIntervalTimeInput.value, + backgroundColor: $subwayLineColorInput.value + } + + + if (updateId) { + validate(data, true); + updateLine(data); + } else { + validate(data, false); + createLine(data); + } + subwayLineModal.toggle(); $subwayLineNameInput.value = ""; + $subwayLineFirstTimeInput.value = ""; + $subwayLineLastTimeInput.value = ""; + $subwayLineIntervalTimeInput.value = ""; $subwayLineColorInput.value = ""; }; + function validate(line, isUpdate) { + if (!line.name || !line.startTime || !line.endTime || !line.intervalTime || !line.backgroundColor) { + alert(ERROR_MESSAGE.NOT_EMPTY); + throw new Error(); + } + if (line.name.includes(" ")) { + alert(ERROR_MESSAGE.NOT_BLANK); + throw new Error(); + } + if (!isUpdate) { + if (duplicatedName(line.name) || duplicatedColor(line.backGroundColor)) { + alert(ERROR_MESSAGE.DUPLICATED); + throw new Error(); + } + } + } + + function duplicatedName(input) { + const names = document.querySelectorAll(".subway-line-item"); + const namesArr = Array.from(names); + return namesArr.some(element => { + return element.innerText.trim() === input; + }); + } + + function duplicatedColor(input) { + const names = document.querySelectorAll(".subway-line-item"); + const namesArr = Array.from(names); + return namesArr.some(element => { + return element.firstElementChild.nextElementSibling.classList[0] === input; + }); + } + const onDeleteSubwayLine = event => { const $target = event.target; const isDeleteButton = $target.classList.contains("mdi-delete"); if (isDeleteButton) { - $target.closest(".subway-line-item").remove(); + const id = $target.parentElement.parentElement.firstElementChild.innerHTML; + fetch("/lines/" + id, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }).then(() => { + $target.closest(".subway-line-item").remove(); + }); } }; + const onReadSubwayLine = event => { + const $target = event.target; + const isSubwayLineItem = $target.classList.contains("subway-line-item"); + if (isSubwayLineItem) { + const subwayLine = { + id: $target.firstElementChild.innerHTML.trim() + }; + fetch("/lines/" + subwayLine.id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then(jsonResponse => { + document.querySelector("#first-time-display").innerText = jsonResponse.startTime.toString() + .substr(0, 5); + document.querySelector("#last-time-display").innerText = jsonResponse.endTime.toString() + .substr(0, 5); + document.querySelector("#interval-time-display").innerText = jsonResponse.intervalTime + "분"; + }).catch(error => error.json()).then(error => alert(error.errorMessage)); + } + } + const onUpdateSubwayLine = event => { + event.preventDefault(); const $target = event.target; const isUpdateButton = $target.classList.contains("mdi-pencil"); if (isUpdateButton) { subwayLineModal.toggle(); + updateId = $target.parentElement.parentElement.firstElementChild.innerHTML; } }; @@ -54,17 +192,30 @@ function AdminLine() { }; const initDefaultSubwayLines = () => { - defaultSubwayLines.map(line => { - $subwayLineList.insertAdjacentHTML( - "beforeend", - subwayLinesTemplate(line) - ); - }); + fetch("/lines", { + method: "GET", + headers: { + 'Content-type': 'application/json' + } + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then(jsonResponse => { + for (const line of jsonResponse) { + $subwayLineList.insertAdjacentHTML("beforeend", subwayLinesTemplate(line)); + } + }).catch(error => error.json()).then(error => alert(error.errorMessage)); + ; + }; const initEventListeners = () => { $subwayLineList.addEventListener(EVENT_TYPE.CLICK, onDeleteSubwayLine); $subwayLineList.addEventListener(EVENT_TYPE.CLICK, onUpdateSubwayLine); + $subwayLineList.addEventListener(EVENT_TYPE.CLICK, onReadSubwayLine); $createSubwayLineButton.addEventListener( EVENT_TYPE.CLICK, onCreateSubwayLine @@ -85,8 +236,8 @@ function AdminLine() { "#subway-line-color-select-container" ); const colorSelectTemplate = subwayLineColorOptions - .map((option, index) => colorSelectOptionTemplate(option, index)) - .join(""); + .map((option, index) => colorSelectOptionTemplate(option, index)) + .join(""); $colorSelectContainer.insertAdjacentHTML("beforeend", colorSelectTemplate); $colorSelectContainer.addEventListener( EVENT_TYPE.CLICK, diff --git a/src/main/resources/static/admin/js/views/AdminStation.js b/src/main/resources/static/admin/js/views/AdminStation.js index 1c290276f..f8da4d451 100644 --- a/src/main/resources/static/admin/js/views/AdminStation.js +++ b/src/main/resources/static/admin/js/views/AdminStation.js @@ -1,36 +1,102 @@ -import { EVENT_TYPE, ERROR_MESSAGE, KEY_TYPE } from "../../utils/constants.js"; +import { ERROR_MESSAGE, EVENT_TYPE, KEY_TYPE } from "../../utils/constants.js"; import { listItemTemplate } from "../../utils/templates.js"; function AdminStation() { const $stationInput = document.querySelector("#station-name"); + const $stationInputButton = document.querySelector("#station-add-btn"); const $stationList = document.querySelector("#station-list"); + const $errorMessage = document.querySelector("#error-message") const onAddStationHandler = event => { - if (event.key !== KEY_TYPE.ENTER) { + if (event.key !== KEY_TYPE.ENTER && event.type !== EVENT_TYPE.CLICK) { return; } event.preventDefault(); const $stationNameInput = document.querySelector("#station-name"); const stationName = $stationNameInput.value; + + validate(stationName); + + let data = { + name: stationName + }; + let statusCode; + + fetch("/stations", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }).then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then(response => { + $stationNameInput.value = ""; + $stationList.insertAdjacentHTML("beforeend", listItemTemplate(response)); + }).catch(error => error.json()).then(error => alert(error.errorMessage)); + }; + + function validate(stationName) { if (!stationName) { alert(ERROR_MESSAGE.NOT_EMPTY); - return; + throw new Error(); } - $stationNameInput.value = ""; - $stationList.insertAdjacentHTML("beforeend", listItemTemplate(stationName)); - }; + if (stationName.includes(" ")) { + alert(ERROR_MESSAGE.NOT_BLANK); + throw new Error(); + } + //myCode + if (/\d/.test(stationName)) { + alert(ERROR_MESSAGE.CONTAIN_NUMBER); + throw new Error(); + } + //myCode + if (duplicatedName(stationName)) { + alert(ERROR_MESSAGE.DUPLICATED); + throw new Error(); + } + } + + function duplicatedName(input) { + const names = document.querySelectorAll(".list-item"); + const namesArr = Array.from(names); + return namesArr.some(element => { + return element.innerText === input; + }); + } const onRemoveStationHandler = event => { const $target = event.target; const isDeleteButton = $target.classList.contains("mdi-delete"); + let statusCode; + if (isDeleteButton) { - $target.closest(".list-item").remove(); + const deleteId = $target.closest(".list-item").dataset.stationId; + fetch('/stations/' + deleteId, { + method: 'DELETE' + }).then(response => { + if (response.status >= 400) { + statusCode = 500; + return response.json(); + } + $target.closest(".list-item").remove(); + }).then(jsonResponse => { + $errorMessage.innerText = jsonResponse.message; + }).catch(error => { + alert(error); + throw new Error(); + }); } }; const initEventListeners = () => { $stationInput.addEventListener(EVENT_TYPE.KEY_PRESS, onAddStationHandler); $stationList.addEventListener(EVENT_TYPE.CLICK, onRemoveStationHandler); + $stationInputButton.addEventListener(EVENT_TYPE.CLICK, onAddStationHandler); }; const init = () => { @@ -43,4 +109,17 @@ function AdminStation() { } const adminStation = new AdminStation(); +const $stationList = document.querySelector("#station-list"); adminStation.init(); +window.onload = async function (event) { + const response = await fetch("/stations", { + method: "GET", + headers: { + 'Content-Type': 'application/json' + } + }); + const jsonResponse = await response.json(); + for (const station of jsonResponse) { + $stationList.insertAdjacentHTML("beforeend", listItemTemplate(station)); + } +}; diff --git a/src/main/resources/static/admin/lib/slider/tiny-slider.css b/src/main/resources/static/admin/lib/slider/tiny-slider.css index 4cb211af8..59d7f41f7 100755 --- a/src/main/resources/static/admin/lib/slider/tiny-slider.css +++ b/src/main/resources/static/admin/lib/slider/tiny-slider.css @@ -137,4 +137,4 @@ float: left; } -/*# sourceMappingURL=sourcemaps/tiny-slider.css.map */ +/*#sourceMappingURL=sourcemaps/tiny-slider.css.map */ diff --git a/src/main/resources/static/admin/ui/CustomModal.js b/src/main/resources/static/admin/ui/CustomModal.js deleted file mode 100644 index a591eb0f8..000000000 --- a/src/main/resources/static/admin/ui/CustomModal.js +++ /dev/null @@ -1,7 +0,0 @@ -import { EVENT_TYPE } from "../utils/constants.js"; -import Modal from "./Modal.js"; - -export default function CustomModal() { - this.toggle = new Modal().toggle - console.log(this.toggle) -} diff --git a/src/main/resources/static/admin/utils/templates.js b/src/main/resources/static/admin/utils/templates.js index b0f17532e..c1e4ff12b 100644 --- a/src/main/resources/static/admin/utils/templates.js +++ b/src/main/resources/static/admin/utils/templates.js @@ -1,15 +1,17 @@ -export const listItemTemplate = value => - `
- ${value} +export const listItemTemplate = (value, lineId) => + `
+ ${value.name}
`; + export const subwayLinesTemplate = line => `
- - ${line.title} + + + ${line.name} @@ -18,39 +20,69 @@ export const subwayLinesTemplate = line =>
`; -export const optionTemplate = value => ``; +export const innerSubwayLinesTemplate = line => + ` + + ${line.name} + + `; + +export const optionTemplate = value => ``; const navTemplate = ``; +const indexNavTemplate = ``; + export const subwayLinesItemTemplate = line => { const stationsTemplate = line.stations - .map(station => listItemTemplate(station)) - .join(""); + .map(station => listItemTemplate(station, line.id)) + .join(""); return `
-
${line.title}
+
${line.name}
${stationsTemplate}
@@ -58,10 +90,15 @@ export const subwayLinesItemTemplate = line => {
`; }; + export const initNavigation = () => { document.querySelector("body").insertAdjacentHTML("afterBegin", navTemplate); }; +export const initIndexNavigation = () => { + document.querySelector("body").insertAdjacentHTML("afterBegin", indexNavTemplate); +}; + export const colorSelectOptionTemplate = (option, index) => { const hasNewLine = ++index % 7 === 0; diff --git a/src/main/resources/static/index/js/AdminApp.js b/src/main/resources/static/index/js/AdminApp.js new file mode 100644 index 000000000..835cd29cd --- /dev/null +++ b/src/main/resources/static/index/js/AdminApp.js @@ -0,0 +1,14 @@ +import { initIndexNavigation } from "../../admin/utils/templates.js"; + +function AdminApp() { + const init = () => { + initIndexNavigation(); + }; + + return { + init + }; +} + +const adminApp = new AdminApp(); +adminApp.init(); diff --git a/src/main/resources/static/service/api/index.js b/src/main/resources/static/service/api/index.js index 0862a9e71..02a7f5731 100644 --- a/src/main/resources/static/service/api/index.js +++ b/src/main/resources/static/service/api/index.js @@ -23,17 +23,21 @@ const METHOD = { } const api = (() => { - const request = (uri, config) => fetch(uri, config).then(data => data.json()) + const request = (uri, config) => fetch(uri, config) + const requestWithJsonData = (uri, config) => fetch(uri, config).then(data => data.json()) const line = { getAll() { return request(`/lines/detail`) + }, + getAllDetail() { + return requestWithJsonData(`/lines/detail`) } } const path = { find(params) { - return request(`/paths?source=${params.source}&target=${params.target}&type=${params.type}`) + return requestWithJsonData(`/paths?source=${params.source}&target=${params.target}&type=${params.type}`) } } diff --git a/src/main/resources/static/service/js/views/Map.js b/src/main/resources/static/service/js/views/Map.js index 49af3f14a..0da4eeda6 100644 --- a/src/main/resources/static/service/js/views/Map.js +++ b/src/main/resources/static/service/js/views/Map.js @@ -1,38 +1,34 @@ -import { optionTemplate, subwayLinesItemTemplate } from '../../utils/templates.js' -import { defaultSubwayLines } from '../../utils/subwayMockData.js' +import { subwayLinesItemTemplate } from '../../utils/templates.js' import tns from '../../lib/slider/tiny-slider.js' +import api from '../../api/index.js' function Map() { const $subwayLinesSlider = document.querySelector('.subway-lines-slider') const initSubwayLinesSlider = () => { - $subwayLinesSlider.innerHTML = defaultSubwayLines.map(line => subwayLinesItemTemplate(line)).join('') - tns({ - container: '.subway-lines-slider', - loop: true, - slideBy: 'page', - speed: 400, - fixedWidth: 300, - autoplayButtonOutput: false, - mouseDrag: true, - lazyload: true, - controlsContainer: '#slider-controls', - items: 3, - edgePadding: 25 - }) - } - - const initSubwayLineOptions = () => { - const subwayLineOptionTemplate = defaultSubwayLines.map(line => optionTemplate(line.title)).join('') - const $stationSelectOptions = document.querySelector('#station-select-options') - $stationSelectOptions.insertAdjacentHTML('afterbegin', subwayLineOptionTemplate) - } + api.line.getAllDetail().then(data => { + const subwayLines = data.lineDetailResponse; + $subwayLinesSlider.innerHTML = subwayLines.map(line => subwayLinesItemTemplate(line)).join(''); + tns({ + container: '.subway-lines-slider', + loop: true, + slideBy: 'page', + speed: 400, + fixedWidth: 300, + autoplayButtonOutput: false, + mouseDrag: true, + lazyLoad: true, + controlsContainer: '#slider-controls', + items: 3, + edgePadding: 25 + }); + }).catch(error => error.json()).then(error => alert(error.errorMessage)); + }; this.init = () => { - initSubwayLinesSlider() - initSubwayLineOptions() + initSubwayLinesSlider(); } } -const edge = new Map() -edge.init() +const edge = new Map(); +edge.init(); diff --git a/src/main/resources/static/service/js/views/Search.js b/src/main/resources/static/service/js/views/Search.js index ecca477d0..512670ea5 100644 --- a/src/main/resources/static/service/js/views/Search.js +++ b/src/main/resources/static/service/js/views/Search.js @@ -1,28 +1,87 @@ import { EVENT_TYPE } from '../../utils/constants.js' +import { + endSearchResultTemplate, + firstSearchResultTemplate, + searchResultTemplate +} from '../../utils/templates.js'; function Search() { - const $departureStationName = document.querySelector('#departure-station-name') - const $arrivalStationName = document.querySelector('#arrival-station-name') - const $searchButton = document.querySelector('#search-button') - const $searchResultContainer = document.querySelector('#search-result-container') - const $favoriteButton = document.querySelector('#favorite-button') - - const showSearchResult = () => { - const isHidden = $searchResultContainer.classList.contains('hidden') + const $departureStationName = document.querySelector('#departure-station-name'); + const $arrivalStationName = document.querySelector('#arrival-station-name'); + const $searchButton = document.querySelector('#search-button'); + const $searchResultContainer = document.querySelector('#search-result-container'); + const $favoriteButton = document.querySelector('#favorite-button'); + const $searchTypeButton = document.querySelectorAll('.search-type-button'); + const $byDurationButton = document.querySelector('#by-duration-button'); + const $byDistanceButton = document.querySelector('#by-distance-button'); + + let $type = "distance"; + + const showSearchResult = (responses) => { + const isHidden = $searchResultContainer.classList.contains('hidden'); + const $pathResult = document.querySelector('#path-list'); + const $totalDistance = document.querySelector('#total-distance'); + const $totalDuration = document.querySelector('#total-duration'); + + + $totalDistance.innerText = responses.totalDistance + "km"; + $totalDuration.innerText = responses.totalDuration + "분"; + const length = responses.stations.length; + $pathResult.innerHTML = firstSearchResultTemplate(responses.stations[0]); + $pathResult.insertAdjacentHTML("beforeend", + responses.stations.slice(1, length - 1).map(searchResultTemplate).join('')); + $pathResult.insertAdjacentHTML("beforeend", + endSearchResultTemplate(responses.stations[length - 1])); + + if (isHidden) { - $searchResultContainer.classList.remove('hidden') + $searchResultContainer.classList.remove('hidden'); } } + const onSearch = event => { - event.preventDefault() - const searchInput = { - source: $departureStationName.value, - target: $arrivalStationName.value + const $target = event.target; + const $pathResult = document.querySelector('#path-list'); + + if ($target.getAttribute("id") === "by-duration-button") { + $type = "duration"; + $target.classList.toggle("bg-gray-200", false); + $target.classList.toggle("border-l", true); + $target.classList.toggle("border-t", true); + $target.classList.toggle("border-r", true); + $byDistanceButton.classList.toggle("bg-gray-200", true); + $byDistanceButton.classList.toggle("border-l", false); + $byDistanceButton.classList.toggle("border-t", false); + $byDistanceButton.classList.toggle("border-r", false); + } + if ($target.getAttribute("id") === "by-distance-button") { + $type = "distance"; + $target.classList.toggle("bg-gray-200", false); + $target.classList.toggle("border-l", true); + $target.classList.toggle("border-t", true); + $target.classList.toggle("border-r", true); + $byDurationButton.classList.toggle("bg-gray-200", true); + $byDurationButton.classList.toggle("border-l", false); + $byDurationButton.classList.toggle("border-t", false); + $byDurationButton.classList.toggle("border-r", false); } - console.log(searchInput) - showSearchResult(searchInput) - } + + event.preventDefault() + fetch("/path?sourceName=" + $departureStationName.value + "&targetName=" + $arrivalStationName.value + "&type=" + $type, + { + method: 'GET', + }).then(response => { + if (!response.ok) { + throw response; + } + response.json().then(jsonResponse => { + showSearchResult(jsonResponse); + }); + }).catch(error => error.json()).then(jsonError => { + alert(jsonError.errorMessage); + }); + }; const onToggleFavorite = event => { event.preventDefault() @@ -43,8 +102,11 @@ function Search() { } const initEventListener = () => { - $favoriteButton.addEventListener(EVENT_TYPE.CLICK, onToggleFavorite) - $searchButton.addEventListener(EVENT_TYPE.CLICK, onSearch) + $favoriteButton.addEventListener(EVENT_TYPE.CLICK, onToggleFavorite); + $searchButton.addEventListener(EVENT_TYPE.CLICK, onSearch); + for (const button of $searchTypeButton) { + button.addEventListener(EVENT_TYPE.CLICK, onSearch); + } } this.init = () => { @@ -54,3 +116,4 @@ function Search() { const login = new Search() login.init() + diff --git a/src/main/resources/static/service/lib/slider/tiny-slider.css b/src/main/resources/static/service/lib/slider/tiny-slider.css old mode 100755 new mode 100644 index 80c022be2..f8ef3a18f --- a/src/main/resources/static/service/lib/slider/tiny-slider.css +++ b/src/main/resources/static/service/lib/slider/tiny-slider.css @@ -137,4 +137,4 @@ float: left; } -/*# sourceMappingURL=sourcemaps/tiny-slider.css.map */ +/*#sourceMappingURL=sourcemaps/tiny-slider.css.map */ diff --git a/src/main/resources/static/service/utils/subwayMockData.js b/src/main/resources/static/service/utils/subwayMockData.js index fb8ec1ed6..128628db9 100644 --- a/src/main/resources/static/service/utils/subwayMockData.js +++ b/src/main/resources/static/service/utils/subwayMockData.js @@ -1,51 +1,3 @@ -export const defaultSubwayLines = [ - { - title: '1호선', - bgColor: 'bg-blue-700', - stations: ['수원', '화서', '성균관대'] - }, - { - title: '2호선', - bgColor: 'bg-green-500', - stations: ['교대', '강남', '역삼', '선릉', '삼성', '종합운동장', '잠실새내', '잠실'] - }, - { - title: '3호선', - bgColor: 'bg-orange-500', - stations: [] - }, - { - title: '4호선', - bgColor: 'bg-blue-500', - stations: [] - }, - { - title: '5호선', - bgColor: 'bg-purple-500', - stations: [] - }, - { - title: '6호선', - bgColor: 'bg-yellow-500', - stations: [] - }, - { - title: '7호선', - bgColor: 'bg-green-500', - stations: [] - }, - { - title: '8호선', - bgColor: 'bg-pink-500', - stations: [] - }, - { - title: '신분당선', - bgColor: 'bg-red-500', - stations: [] - } -] - export const defaultFavorites = [ { departureStation: '잠실역', diff --git a/src/main/resources/static/service/utils/templates.js b/src/main/resources/static/service/utils/templates.js index b289dea1d..ad7575363 100644 --- a/src/main/resources/static/service/utils/templates.js +++ b/src/main/resources/static/service/utils/templates.js @@ -1,6 +1,6 @@ export const listItemTemplate = value => `
- ${value} + ${value.name} @@ -20,12 +20,12 @@ const navTemplate = () => `
-
-
- -
-
-
-
+ + + subway admin + + + + + + + + +
+
+
+
+
구간 관리
+
+ +
+
+
+ +
+
+
+
- diff --git a/src/main/resources/templates/service/map.html b/src/main/resources/templates/service/map.html index 0c332a300..aba19759d 100644 --- a/src/main/resources/templates/service/map.html +++ b/src/main/resources/templates/service/map.html @@ -6,9 +6,9 @@ - - - + + +
@@ -42,6 +42,6 @@
- + diff --git a/src/main/resources/templates/service/search.html b/src/main/resources/templates/service/search.html index 9493a5e4a..c7c46fffa 100644 --- a/src/main/resources/templates/service/search.html +++ b/src/main/resources/templates/service/search.html @@ -6,8 +6,8 @@ - - + +
@@ -52,10 +52,10 @@
-
- - 강남 - - - 역삼 - - 선릉 - - 삼성 - - - 잠실 - +
@@ -95,7 +82,7 @@
- - + + diff --git a/src/test/java/wooteco/subway/admin/acceptance/AcceptanceTest.java b/src/test/java/wooteco/subway/admin/acceptance/AcceptanceTest.java index 989151a8c..fefe8b09a 100644 --- a/src/test/java/wooteco/subway/admin/acceptance/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/admin/acceptance/AcceptanceTest.java @@ -1,20 +1,26 @@ package wooteco.subway.admin.acceptance; -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.context.jdbc.Sql; -import wooteco.subway.admin.dto.*; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; +import wooteco.subway.admin.dto.ErrorResponse; +import wooteco.subway.admin.dto.LineDetailResponse; +import wooteco.subway.admin.dto.LineResponse; +import wooteco.subway.admin.dto.PathResponse; +import wooteco.subway.admin.dto.StationResponse; +import wooteco.subway.admin.dto.WholeSubwayResponse; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql("/truncate.sql") @@ -45,62 +51,72 @@ StationResponse createStation(String name) { params.put("name", name); return - given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). + given(). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). when(). - post("/stations"). + post("/stations"). then(). - log().all(). - statusCode(HttpStatus.CREATED.value()). - extract().as(StationResponse.class); + log().all(). + statusCode(HttpStatus.CREATED.value()). + extract().as(StationResponse.class); } List getStations() { return - given().when(). - get("/stations"). + given().when(). + get("/stations"). then(). - log().all(). - extract(). - jsonPath().getList(".", StationResponse.class); + log().all(). + extract(). + jsonPath().getList(".", StationResponse.class); } void deleteStation(Long id) { given().when(). - delete("/stations/" + id). - then(). - log().all(); + delete("/stations/" + id). + then(). + log().all(); } LineResponse createLine(String name) { Map params = new HashMap<>(); params.put("name", name); + params.put("backgroundColor", "white"); params.put("startTime", LocalTime.of(5, 30).format(DateTimeFormatter.ISO_LOCAL_TIME)); params.put("endTime", LocalTime.of(23, 30).format(DateTimeFormatter.ISO_LOCAL_TIME)); params.put("intervalTime", "10"); return - given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). + given(). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). when(). - post("/lines"). + post("/lines"). + then(). + log().all(). + statusCode(HttpStatus.CREATED.value()). + extract().as(LineResponse.class); + } + + LineResponse getLine(Long id) { + return + given().when(). + get("/lines/" + id). then(). - log().all(). - statusCode(HttpStatus.CREATED.value()). - extract().as(LineResponse.class); + log().all(). + extract().as(LineResponse.class); } - LineDetailResponse getLine(Long id) { + LineDetailResponse getDetailLine(Long id) { return - given().when(). - get("/lines/" + id). + given().when(). + get("/lines/detail/" + id). then(). - log().all(). - extract().as(LineDetailResponse.class); + log().all(). + extract().as(LineDetailResponse.class); } void updateLine(Long id, LocalTime startTime, LocalTime endTime) { @@ -110,38 +126,49 @@ void updateLine(Long id, LocalTime startTime, LocalTime endTime) { params.put("intervalTime", "10"); given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - put("/lines/" + id). - then(). - log().all(). - statusCode(HttpStatus.OK.value()); + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + put("/lines/" + id). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); } List getLines() { return - given().when(). - get("/lines"). + given().when(). + get("/lines"). + then(). + log().all(). + extract(). + jsonPath().getList(".", LineResponse.class); + } + + WholeSubwayResponse getDetailLines() { + return + given().when(). + get("/lines/detail"). then(). - log().all(). - extract(). - jsonPath().getList(".", LineResponse.class); + log().all(). + extract(). + as(WholeSubwayResponse.class); } void deleteLine(Long id) { given().when(). - delete("/lines/" + id). - then(). - log().all(); + delete("/lines/" + id). + then(). + log().all(); } void addLineStation(Long lineId, Long preStationId, Long stationId) { addLineStation(lineId, preStationId, stationId, 10, 10); } - void addLineStation(Long lineId, Long preStationId, Long stationId, Integer distance, Integer duration) { + void addLineStation(Long lineId, Long preStationId, Long stationId, Integer distance, + Integer duration) { Map params = new HashMap<>(); params.put("preStationId", preStationId == null ? "" : preStationId.toString()); params.put("stationId", stationId.toString()); @@ -149,25 +176,52 @@ void addLineStation(Long lineId, Long preStationId, Long stationId, Integer dist params.put("duration", duration.toString()); given(). - body(params). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + post("/lines/" + lineId + "/stations"). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); + } + + void removeLineStation(Long lineId, Long stationId) { + given(). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + delete("/lines/" + lineId + "/stations/" + stationId). + then(). + log().all(). + statusCode(HttpStatus.NO_CONTENT.value()); + } + + PathResponse findPath(String source, String target, String type) { + String encodingUrl = + "/path?sourceName=" + source + "&targetName=" + target + "&type=" + type; + return + given(). contentType(MediaType.APPLICATION_JSON_VALUE). accept(MediaType.APPLICATION_JSON_VALUE). when(). - post("/lines/" + lineId + "/stations"). + get(encodingUrl). then(). log().all(). - statusCode(HttpStatus.OK.value()); + statusCode(HttpStatus.OK.value()). + extract().as(PathResponse.class); } - void removeLineStation(Long lineId, Long stationId) { - given(). - contentType(MediaType.APPLICATION_JSON_VALUE). + ErrorResponse findPathByWrongType(String source, String target, String type) { + return + given(). accept(MediaType.APPLICATION_JSON_VALUE). when(). - delete("/lines/" + lineId + "/stations/" + stationId). + get("/path?sourceName=" + source + "&targetName=" + target + "&type=" + type). then(). log().all(). - statusCode(HttpStatus.NO_CONTENT.value()); + statusCode(HttpStatus.BAD_REQUEST.value()). + extract().as(ErrorResponse.class); } } diff --git a/src/test/java/wooteco/subway/admin/acceptance/LineAcceptanceTest.java b/src/test/java/wooteco/subway/admin/acceptance/LineAcceptanceTest.java index 68baff4d0..b0b9ca267 100644 --- a/src/test/java/wooteco/subway/admin/acceptance/LineAcceptanceTest.java +++ b/src/test/java/wooteco/subway/admin/acceptance/LineAcceptanceTest.java @@ -24,7 +24,7 @@ void manageLine() { assertThat(lines.size()).isEqualTo(4); // when - LineDetailResponse line = getLine(lines.get(0).getId()); + LineResponse line = getLine(lines.get(0).getId()); // then assertThat(line.getId()).isNotNull(); assertThat(line.getName()).isNotNull(); @@ -37,7 +37,7 @@ void manageLine() { LocalTime endTime = LocalTime.of(22, 00); updateLine(line.getId(), startTime, endTime); //then - LineDetailResponse updatedLine = getLine(line.getId()); + LineResponse updatedLine = getLine(line.getId()); assertThat(updatedLine.getStartTime()).isEqualTo(startTime); assertThat(updatedLine.getEndTime()).isEqualTo(endTime); diff --git a/src/test/java/wooteco/subway/admin/acceptance/LineStationAcceptanceTest.java b/src/test/java/wooteco/subway/admin/acceptance/LineStationAcceptanceTest.java index b937e365a..0dfc07ce7 100644 --- a/src/test/java/wooteco/subway/admin/acceptance/LineStationAcceptanceTest.java +++ b/src/test/java/wooteco/subway/admin/acceptance/LineStationAcceptanceTest.java @@ -23,12 +23,12 @@ void manageLineStation() { addLineStation(lineResponse.getId(), stationResponse1.getId(), stationResponse2.getId()); addLineStation(lineResponse.getId(), stationResponse2.getId(), stationResponse3.getId()); - LineDetailResponse lineDetailResponse = getLine(lineResponse.getId()); + LineDetailResponse lineDetailResponse = getDetailLine(lineResponse.getId()); assertThat(lineDetailResponse.getStations()).hasSize(3); removeLineStation(lineResponse.getId(), stationResponse2.getId()); - LineDetailResponse lineResponseAfterRemoveLineStation = getLine(lineResponse.getId()); + LineDetailResponse lineResponseAfterRemoveLineStation = getDetailLine(lineResponse.getId()); assertThat(lineResponseAfterRemoveLineStation.getStations().size()).isEqualTo(2); } } diff --git a/src/test/java/wooteco/subway/admin/acceptance/PageAcceptanceTest.java b/src/test/java/wooteco/subway/admin/acceptance/PageAcceptanceTest.java index 01c068334..c956134ce 100644 --- a/src/test/java/wooteco/subway/admin/acceptance/PageAcceptanceTest.java +++ b/src/test/java/wooteco/subway/admin/acceptance/PageAcceptanceTest.java @@ -2,6 +2,8 @@ import io.restassured.RestAssured; import io.restassured.specification.RequestSpecification; +import wooteco.subway.admin.dto.LineResponse; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -17,7 +19,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql("/truncate.sql") -public class PageAcceptanceTest { +public class PageAcceptanceTest extends AcceptanceTest{ @LocalServerPort int port; @@ -37,28 +39,57 @@ void linePage() { given(). accept(MediaType.TEXT_HTML_VALUE). when(). - get("/lines"). + get("/admin/line"). then(). log().all(). statusCode(HttpStatus.OK.value()); } - private void createLine(String name) { - Map params = new HashMap<>(); - params.put("name", name); - params.put("startTime", LocalTime.of(5, 30).format(DateTimeFormatter.ISO_LOCAL_TIME)); - params.put("endTime", LocalTime.of(23, 30).format(DateTimeFormatter.ISO_LOCAL_TIME)); - params.put("intervalTime", "10"); + @Test + void stationPage() { + createStation("강남"); + given(). + accept(MediaType.TEXT_HTML_VALUE). + when(). + get("/admin/station"). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); + } + + @Test + void edgePage() { given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - post("/lines"). - then(). - log().all(). - statusCode(HttpStatus.CREATED.value()); + accept(MediaType.TEXT_HTML_VALUE). + when(). + get("/admin/edge"). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); } + @Test + void mapPage() { + + given(). + accept(MediaType.TEXT_HTML_VALUE). + when(). + get("/service/map"). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); + } + + @Test + void searchPage() { + + given(). + accept(MediaType.TEXT_HTML_VALUE). + when(). + get("/service/search"). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); + } } diff --git a/src/test/java/wooteco/subway/admin/acceptance/PathAcceptanceTest.java b/src/test/java/wooteco/subway/admin/acceptance/PathAcceptanceTest.java new file mode 100644 index 000000000..819ee7d6f --- /dev/null +++ b/src/test/java/wooteco/subway/admin/acceptance/PathAcceptanceTest.java @@ -0,0 +1,67 @@ +package wooteco.subway.admin.acceptance; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import wooteco.subway.admin.dto.ErrorResponse; +import wooteco.subway.admin.dto.LineResponse; +import wooteco.subway.admin.dto.PathResponse; +import wooteco.subway.admin.dto.StationResponse; + +public class PathAcceptanceTest extends AcceptanceTest { + + @DisplayName("경로 조회 API") + @Test + void findPath() { + //given + StationResponse jamsil = createStation("잠실"); + StationResponse jamsilsaenae = createStation("잠실새내"); + StationResponse playgound = createStation("종합운동장"); + StationResponse samjun = createStation("삼전"); + StationResponse sukchongobun = createStation("석촌고분"); + StationResponse sukchon = createStation("석촌"); + + LineResponse line2 = createLine("2호선"); + LineResponse line8 = createLine("8호선"); + LineResponse line9 = createLine("9호선"); + + addLineStation(line2.getId(), null, jamsil.getId(), 0, 0); + addLineStation(line2.getId(), jamsil.getId(), jamsilsaenae.getId(), 10, 1); + addLineStation(line2.getId(), jamsilsaenae.getId(), playgound.getId(), 10, 1); + + addLineStation(line9.getId(), null, playgound.getId(), 0, 0); + addLineStation(line9.getId(), playgound.getId(), samjun.getId(), 10, 1); + addLineStation(line9.getId(), samjun.getId(), sukchongobun.getId(), 1, 10); + addLineStation(line9.getId(), sukchongobun.getId(), sukchon.getId(), 1, 10); + + addLineStation(line8.getId(), null, jamsil.getId(), 0, 0); + addLineStation(line8.getId(), jamsil.getId(), sukchon.getId(), 1, 10); + + //when + PathResponse pathByDistance = findPath(jamsil.getName(), samjun.getName(), "distance"); + + //then + assertThat(pathByDistance.getStations()).hasSize(4); + assertThat(pathByDistance.getStations()).extracting(StationResponse::getName) + .containsExactly("잠실", "석촌", "석촌고분", "삼전"); + assertThat(pathByDistance.getTotalDistance()).isEqualTo(3); + assertThat(pathByDistance.getTotalDuration()).isEqualTo(30); + + //when + PathResponse pathByDuration = findPath(jamsil.getName(), samjun.getName(), "duration"); + + //then + assertThat(pathByDuration.getStations()).hasSize(4); + assertThat(pathByDuration.getStations()).extracting(StationResponse::getName) + .containsExactly("잠실", "잠실새내", "종합운동장", "삼전"); + assertThat(pathByDuration.getTotalDistance()).isEqualTo(30); + assertThat(pathByDuration.getTotalDuration()).isEqualTo(3); + + //when + assertThat(findPathByWrongType(jamsil.getName(), samjun.getName(), "transfer")) + .isInstanceOf(ErrorResponse.class).extracting(ErrorResponse::getErrorMessage) + .isEqualTo("transfer방식의 경로는 지원하지 않습니다."); + } +} diff --git a/src/test/java/wooteco/subway/admin/acceptance/StationAcceptanceTest.java b/src/test/java/wooteco/subway/admin/acceptance/StationAcceptanceTest.java index d0fbe884b..3a562c5ba 100644 --- a/src/test/java/wooteco/subway/admin/acceptance/StationAcceptanceTest.java +++ b/src/test/java/wooteco/subway/admin/acceptance/StationAcceptanceTest.java @@ -19,11 +19,15 @@ void manageStation() { // then List stations = getStations(); assertThat(stations.size()).isEqualTo(3); + assertThat(stations).extracting(StationResponse::getName) + .containsExactly(STATION_NAME_KANGNAM, STATION_NAME_YEOKSAM, STATION_NAME_SEOLLEUNG); // when deleteStation(stations.get(0).getId()); // then List stationsAfterDelete = getStations(); assertThat(stationsAfterDelete.size()).isEqualTo(2); + assertThat(stationsAfterDelete).extracting(StationResponse::getName) + .containsExactly(STATION_NAME_YEOKSAM, STATION_NAME_SEOLLEUNG); } } diff --git a/src/test/java/wooteco/subway/admin/acceptance/WholeSubwayAcceptanceTest.java b/src/test/java/wooteco/subway/admin/acceptance/WholeSubwayAcceptanceTest.java new file mode 100644 index 000000000..ca8d1e492 --- /dev/null +++ b/src/test/java/wooteco/subway/admin/acceptance/WholeSubwayAcceptanceTest.java @@ -0,0 +1,54 @@ +package wooteco.subway.admin.acceptance; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import wooteco.subway.admin.dto.LineDetailResponse; +import wooteco.subway.admin.dto.LineResponse; +import wooteco.subway.admin.dto.StationResponse; +import wooteco.subway.admin.dto.WholeSubwayResponse; + +public class WholeSubwayAcceptanceTest extends AcceptanceTest { + + @DisplayName("지하철 노선도 전체 정보 조회") + @Test + public void wholeSubway() { + //given + LineResponse lineResponse1 = createLine("2호선"); + StationResponse stationResponse1 = createStation("강남역"); + StationResponse stationResponse2 = createStation("역삼역"); + StationResponse stationResponse3 = createStation("삼성역"); + addLineStation(lineResponse1.getId(), null, stationResponse1.getId()); + addLineStation(lineResponse1.getId(), stationResponse1.getId(), stationResponse2.getId()); + addLineStation(lineResponse1.getId(), stationResponse2.getId(), stationResponse3.getId()); + + LineResponse lineResponse2 = createLine("신분당선"); + StationResponse stationResponse5 = createStation("양재역"); + StationResponse stationResponse6 = createStation("양재시민의숲역"); + addLineStation(lineResponse2.getId(), null, stationResponse1.getId()); + addLineStation(lineResponse2.getId(), stationResponse1.getId(), stationResponse5.getId()); + addLineStation(lineResponse2.getId(), stationResponse5.getId(), stationResponse6.getId()); + + // when + List response = retrieveWholeSubway().getLineDetailResponse(); + + // then + assertThat(response.size()).isEqualTo(2); + assertThat(response.get(0).getStations().size()).isEqualTo(3); + assertThat(response.get(1).getStations().size()).isEqualTo(3); + } + + private WholeSubwayResponse retrieveWholeSubway() { + return given().when(). + get("/lines/detail"). + then(). + statusCode(HttpStatus.OK.value()). + extract(). + as(WholeSubwayResponse.class); + } +} diff --git a/src/test/java/wooteco/subway/admin/api/LineControllerTest.java b/src/test/java/wooteco/subway/admin/api/LineControllerTest.java index e651053d0..47a14daae 100644 --- a/src/test/java/wooteco/subway/admin/api/LineControllerTest.java +++ b/src/test/java/wooteco/subway/admin/api/LineControllerTest.java @@ -14,6 +14,7 @@ import wooteco.subway.admin.dto.LineDetailResponse; import wooteco.subway.admin.dto.WholeSubwayResponse; import wooteco.subway.admin.service.LineService; +import wooteco.subway.admin.service.StationService; import java.util.Arrays; import java.util.List; @@ -33,27 +34,29 @@ public class LineControllerTest { @MockBean private LineService lineService; + @MockBean + private StationService stationService; + @Test void ETag() throws Exception { WholeSubwayResponse response = WholeSubwayResponse.of(Arrays.asList(createMockResponse(), createMockResponse())); - given(lineService.wholeLines()).willReturn(response); + given(lineService.findDetailLines()).willReturn(response); - // TODO: 전체 지하철 노선도 정보를 조회하는 URI 입력하기 - String uri = ""; + String uri = "/lines/detail"; MvcResult mvcResult = mockMvc.perform(get(uri)) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(header().exists("ETag")) - .andReturn(); + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().exists("ETag")) + .andReturn(); String eTag = mvcResult.getResponse().getHeader("ETag"); mockMvc.perform(get(uri).header("If-None-Match", eTag)) - .andDo(print()) - .andExpect(status().isNotModified()) - .andExpect(header().exists("ETag")) - .andReturn(); + .andDo(print()) + .andExpect(status().isNotModified()) + .andExpect(header().exists("ETag")) + .andReturn(); } private LineDetailResponse createMockResponse() { diff --git a/src/test/java/wooteco/subway/admin/domain/GraphTest.java b/src/test/java/wooteco/subway/admin/domain/GraphTest.java new file mode 100644 index 000000000..c7dbd2a79 --- /dev/null +++ b/src/test/java/wooteco/subway/admin/domain/GraphTest.java @@ -0,0 +1,72 @@ +package wooteco.subway.admin.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalTime; +import java.util.Arrays; + +import org.jgrapht.graph.WeightedMultigraph; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.jdbc.Sql; + +@Sql("/truncate.sql") +class GraphTest { + + private Line line1; + private Line line2; + + private WeightedMultigraph graphByDistance; + private WeightedMultigraph graphByDuration; + + @BeforeEach + void setUp() { + line1 = Line.of(1L, "2호선", "bg-green-400", LocalTime.of(5, 30), LocalTime.of(22, 30), 5); + line1.addLineStation(new LineStation(null, 1L, 5, 10)); + line1.addLineStation(new LineStation(1L, 2L, 5, 10)); + line1.addLineStation(new LineStation(2L, 3L, 5, 10)); + + line2 = Line.of(2L, "분당선", "bg-yellow-500", LocalTime.of(5, 30), LocalTime.of(22, 30), 5); + line2.addLineStation(new LineStation(null, 4L, 10, 5)); + line2.addLineStation(new LineStation(4L, 2L, 10, 5)); + line2.addLineStation(new LineStation(2L, 5L, 10, 5)); + + graphByDistance = Graph.of(Arrays.asList(line1, line2), PathType.DISTANCE).getGraph(); + graphByDuration = Graph.of(Arrays.asList(line1, line2), PathType.DURATION).getGraph(); + } + + @Test + @DisplayName("그래프에 vertex가 존재한다") + void vertex() { + assertThat(graphByDistance.vertexSet()).containsExactly(1L, 2L, 3L, 4L, 5L); + assertThat(graphByDuration.vertexSet()).containsExactly(1L, 2L, 3L, 4L, 5L); + } + + @Test + @DisplayName("그래프에 Edge가 존재한다") + void edge() { + assertThat(graphByDistance.containsEdge(1L, 2L)).isTrue(); + assertThat(graphByDistance.containsEdge(2L, 3L)).isTrue(); + assertThat(graphByDistance.containsEdge(4L, 2L)).isTrue(); + assertThat(graphByDistance.containsEdge(2L, 5L)).isTrue(); + + assertThat(graphByDuration.containsEdge(1L, 2L)).isTrue(); + assertThat(graphByDuration.containsEdge(2L, 3L)).isTrue(); + assertThat(graphByDuration.containsEdge(4L, 2L)).isTrue(); + assertThat(graphByDuration.containsEdge(2L, 5L)).isTrue(); + } + + @Test + void weight() { + assertThat(graphByDistance.getEdge(1L, 2L).getWeight()).isEqualTo(5); + assertThat(graphByDistance.getEdge(2L, 3L).getWeight()).isEqualTo(5); + assertThat(graphByDistance.getEdge(4L, 2L).getWeight()).isEqualTo(10); + assertThat(graphByDistance.getEdge(2L, 5L).getWeight()).isEqualTo(10); + + assertThat(graphByDuration.getEdge(1L, 2L).getWeight()).isEqualTo(10); + assertThat(graphByDuration.getEdge(2L, 3L).getWeight()).isEqualTo(10); + assertThat(graphByDuration.getEdge(4L, 2L).getWeight()).isEqualTo(5); + assertThat(graphByDuration.getEdge(2L, 5L).getWeight()).isEqualTo(5); + } +} \ No newline at end of file diff --git a/src/test/java/wooteco/subway/admin/domain/LineTest.java b/src/test/java/wooteco/subway/admin/domain/LineTest.java index fb18f4502..e64e17c8a 100644 --- a/src/test/java/wooteco/subway/admin/domain/LineTest.java +++ b/src/test/java/wooteco/subway/admin/domain/LineTest.java @@ -1,21 +1,24 @@ package wooteco.subway.admin.domain; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.*; import java.time.LocalTime; import java.util.List; +import java.util.Objects; -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.test.context.jdbc.Sql; +@Sql("/truncate.sql") public class LineTest { private Line line; @BeforeEach void setUp() { - line = new Line(1L, "2호선", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); + line = Line.of(1L, "2호선", "white", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); line.addLineStation(new LineStation(null, 1L, 10, 10)); line.addLineStation(new LineStation(1L, 2L, 10, 10)); line.addLineStation(new LineStation(2L, 3L, 10, 10)); @@ -27,8 +30,8 @@ void addLineStation() { assertThat(line.getStations()).hasSize(4); LineStation lineStation = line.getStations().stream() - .filter(it -> it.getPreStationId() == 4L) - .findFirst() + .filter(it -> Objects.nonNull(it.getPreStationId()) && it.getPreStationId().equals(4L)) + .findAny() .orElseThrow(RuntimeException::new); assertThat(lineStation.getStationId()).isEqualTo(1L); } diff --git a/src/test/java/wooteco/subway/admin/domain/PathAlgorithmByDijkstraTest.java b/src/test/java/wooteco/subway/admin/domain/PathAlgorithmByDijkstraTest.java new file mode 100644 index 000000000..14384fe58 --- /dev/null +++ b/src/test/java/wooteco/subway/admin/domain/PathAlgorithmByDijkstraTest.java @@ -0,0 +1,62 @@ +package wooteco.subway.admin.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalTime; +import java.util.Arrays; + +import org.jgrapht.graph.WeightedMultigraph; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.jdbc.Sql; + +@Sql("/truncate.sql") +class PathAlgorithmByDijkstraTest { + + private Line line1; + private Line line2; + + private WeightedMultigraph graphByDistance; + private WeightedMultigraph graphByDuration; + + private PathAlgorithm pathAlgorithm; + + @BeforeEach + void setUp() { + line1 = Line.of(1L, "2호선", "bg-green-400", LocalTime.of(5, 30), LocalTime.of(22, 30), 5); + line1.addLineStation(new LineStation(null, 1L, 0, 0)); + line1.addLineStation(new LineStation(1L, 2L, 5, 10)); + line1.addLineStation(new LineStation(2L, 3L, 5, 10)); + line1.addLineStation(new LineStation(3L, 6L, 5, 10)); + + line2 = Line.of(2L, "분당선", "bg-yellow-500", LocalTime.of(5, 30), LocalTime.of(22, 30), 5); + line2.addLineStation(new LineStation(null, 1L, 0, 0)); + line2.addLineStation(new LineStation(1L, 4L, 10, 5)); + line2.addLineStation(new LineStation(4L, 5L, 10, 5)); + line2.addLineStation(new LineStation(5L, 6L, 10, 5)); + + graphByDistance = Graph.of(Arrays.asList(line1, line2), PathType.DISTANCE).getGraph(); + graphByDuration = Graph.of(Arrays.asList(line1, line2), PathType.DURATION).getGraph(); + + pathAlgorithm = new PathAlgorithmByDijkstra(); + } + + @Test + @DisplayName("최단 거리 경로를 구한다") + void getShortestPathByDistance() { + PathResult path = pathAlgorithm.findPath(1L, 6L, Graph.of(graphByDistance)); + assertThat(path.getPath()).containsExactly(1L, 2L, 3L, 6L); + assertThat(path.getTotalDistance()).isEqualTo(15); + assertThat(path.getTotalDuration()).isEqualTo(30); + } + + @Test + @DisplayName("최소 시간 경로를 구한다") + void getShortestPathByDuration() { + PathResult path = pathAlgorithm.findPath(1L, 6L, Graph.of(graphByDuration)); + assertThat(path.getPath()).containsExactly(1L, 4L, 5L, 6L); + assertThat(path.getTotalDistance()).isEqualTo(30); + assertThat(path.getTotalDuration()).isEqualTo(15); + } +} \ No newline at end of file diff --git a/src/test/java/wooteco/subway/admin/repository/LineRepositoryTest.java b/src/test/java/wooteco/subway/admin/repository/LineRepositoryTest.java index fade26c04..3ef260d93 100644 --- a/src/test/java/wooteco/subway/admin/repository/LineRepositoryTest.java +++ b/src/test/java/wooteco/subway/admin/repository/LineRepositoryTest.java @@ -1,15 +1,16 @@ package wooteco.subway.admin.repository; +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalTime; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; + import wooteco.subway.admin.domain.Line; import wooteco.subway.admin.domain.LineStation; -import java.time.LocalTime; - -import static org.assertj.core.api.Assertions.assertThat; - @DataJdbcTest public class LineRepositoryTest { @Autowired @@ -18,7 +19,7 @@ public class LineRepositoryTest { @Test void addLineStation() { // given - Line line = new Line("2호선", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); + Line line = Line.of(1L, "2호선", "white", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); Line persistLine = lineRepository.save(line); persistLine.addLineStation(new LineStation(null, 1L, 10, 10)); persistLine.addLineStation(new LineStation(1L, 2L, 10, 10)); diff --git a/src/test/java/wooteco/subway/admin/repository/StationRepositoryTest.java b/src/test/java/wooteco/subway/admin/repository/StationRepositoryTest.java index c670f332f..5fd10ec0f 100644 --- a/src/test/java/wooteco/subway/admin/repository/StationRepositoryTest.java +++ b/src/test/java/wooteco/subway/admin/repository/StationRepositoryTest.java @@ -1,12 +1,13 @@ package wooteco.subway.admin.repository; +import static org.junit.jupiter.api.Assertions.*; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; import org.springframework.data.relational.core.conversion.DbActionExecutionException; -import wooteco.subway.admin.domain.Station; -import static org.junit.jupiter.api.Assertions.assertThrows; +import wooteco.subway.admin.domain.Station; @DataJdbcTest public class StationRepositoryTest { @@ -18,6 +19,7 @@ void saveStation() { String stationName = "강남역"; stationRepository.save(new Station(stationName)); - assertThrows(DbActionExecutionException.class, () -> stationRepository.save(new Station(stationName))); + assertThrows(DbActionExecutionException.class, + () -> stationRepository.save(new Station(stationName))); } } diff --git a/src/test/java/wooteco/subway/admin/service/LineServiceTest.java b/src/test/java/wooteco/subway/admin/service/LineServiceTest.java index bc9a5f4f2..9bbf8e096 100644 --- a/src/test/java/wooteco/subway/admin/service/LineServiceTest.java +++ b/src/test/java/wooteco/subway/admin/service/LineServiceTest.java @@ -1,27 +1,30 @@ package wooteco.subway.admin.service; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; + import org.assertj.core.util.Lists; +import org.assertj.core.util.Sets; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import wooteco.subway.admin.domain.Line; import wooteco.subway.admin.domain.LineStation; import wooteco.subway.admin.domain.Station; import wooteco.subway.admin.dto.LineDetailResponse; import wooteco.subway.admin.dto.LineStationCreateRequest; import wooteco.subway.admin.repository.LineRepository; -import wooteco.subway.admin.repository.StationRepository; - -import java.time.LocalTime; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class LineServiceTest { @@ -33,7 +36,7 @@ public class LineServiceTest { @Mock private LineRepository lineRepository; @Mock - private StationRepository stationRepository; + private StationService stationService; private LineService lineService; @@ -45,14 +48,14 @@ public class LineServiceTest { @BeforeEach void setUp() { - lineService = new LineService(lineRepository, stationRepository); + lineService = new LineService(lineRepository, stationService); station1 = new Station(1L, STATION_NAME1); station2 = new Station(2L, STATION_NAME2); station3 = new Station(3L, STATION_NAME3); station4 = new Station(4L, STATION_NAME4); - line = new Line(1L, "2호선", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); + line = Line.of(1L, "2호선", "white", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); line.addLineStation(new LineStation(null, 1L, 10, 10)); line.addLineStation(new LineStation(1L, 2L, 10, 10)); line.addLineStation(new LineStation(2L, 3L, 10, 10)); @@ -62,7 +65,8 @@ void setUp() { void addLineStationAtTheFirstOfLine() { when(lineRepository.findById(line.getId())).thenReturn(Optional.of(line)); - LineStationCreateRequest request = new LineStationCreateRequest(null, station4.getId(), 10, 10); + LineStationCreateRequest request = new LineStationCreateRequest(null, station4.getId(), 10, + 10); lineService.addLineStation(line.getId(), request); assertThat(line.getStations()).hasSize(4); @@ -78,7 +82,8 @@ void addLineStationAtTheFirstOfLine() { void addLineStationBetweenTwo() { when(lineRepository.findById(line.getId())).thenReturn(Optional.of(line)); - LineStationCreateRequest request = new LineStationCreateRequest(station1.getId(), station4.getId(), 10, 10); + LineStationCreateRequest request = new LineStationCreateRequest(station1.getId(), + station4.getId(), 10, 10); lineService.addLineStation(line.getId(), request); assertThat(line.getStations()).hasSize(4); @@ -94,7 +99,8 @@ void addLineStationBetweenTwo() { void addLineStationAtTheEndOfLine() { when(lineRepository.findById(line.getId())).thenReturn(Optional.of(line)); - LineStationCreateRequest request = new LineStationCreateRequest(station3.getId(), station4.getId(), 10, 10); + LineStationCreateRequest request = new LineStationCreateRequest(station3.getId(), + station4.getId(), 10, 10); lineService.addLineStation(line.getId(), request); assertThat(line.getStations()).hasSize(4); @@ -142,12 +148,38 @@ void removeLineStationAtTheEndOfLine() { @Test void findLineWithStationsById() { - List stations = Lists.newArrayList(new Station("강남역"), new Station("역삼역"), new Station("삼성역")); + List stations = Lists.newArrayList(new Station("강남역"), new Station("역삼역"), + new Station("삼성역")); when(lineRepository.findById(anyLong())).thenReturn(Optional.of(line)); - when(stationRepository.findAllById(anyList())).thenReturn(stations); + when(stationService.findAllById(anyList())).thenReturn(stations); - LineDetailResponse lineDetailResponse = lineService.findLineWithStationsById(1L); + LineDetailResponse lineDetailResponse = lineService.findDetailLine(1L); assertThat(lineDetailResponse.getStations()).hasSize(3); } + + @Test + void wholeLines() { + Line newLine = Line.of(2L, "신분당선", "black", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); + newLine.addLineStation(new LineStation(null, 4L, 10, 10)); + newLine.addLineStation(new LineStation(4L, 5L, 10, 10)); + newLine.addLineStation(new LineStation(5L, 6L, 10, 10)); + + Set stations1 = Sets.newLinkedHashSet(new Station(1L, "강남역"), + new Station(2L, "역삼역"), new Station(3L, "삼성역")); + Set stations2 = Sets.newLinkedHashSet(new Station(4L, "양재역"), + new Station(5L, "양재시민의숲역"), new Station(6L, "청계산입구역")); + + when(lineRepository.findAll()).thenReturn(Arrays.asList(this.line, newLine)); + when(stationService.findAllById(line.getLineStationsId())).thenReturn( + new ArrayList<>(stations1)); + when(stationService.findAllById(newLine.getLineStationsId())).thenReturn( + new ArrayList<>(stations2)); + + List lineDetails = lineService.findDetailLines().getLineDetailResponse(); + + assertThat(lineDetails).isNotNull(); + assertThat(lineDetails.get(0).getStations().size()).isEqualTo(3); + assertThat(lineDetails.get(1).getStations().size()).isEqualTo(3); + } } diff --git a/src/test/java/wooteco/subway/admin/service/PathServiceTest.java b/src/test/java/wooteco/subway/admin/service/PathServiceTest.java new file mode 100644 index 000000000..eb18a5483 --- /dev/null +++ b/src/test/java/wooteco/subway/admin/service/PathServiceTest.java @@ -0,0 +1,172 @@ +package wooteco.subway.admin.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalTime; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import wooteco.subway.admin.domain.Line; +import wooteco.subway.admin.domain.LineStation; +import wooteco.subway.admin.domain.PathAlgorithm; +import wooteco.subway.admin.domain.PathAlgorithmByDijkstra; +import wooteco.subway.admin.domain.PathType; +import wooteco.subway.admin.domain.Station; +import wooteco.subway.admin.dto.PathRequest; +import wooteco.subway.admin.dto.PathResponse; +import wooteco.subway.admin.dto.StationResponse; +import wooteco.subway.admin.exception.IllegalStationNameException; +import wooteco.subway.admin.exception.NotFoundLineException; +import wooteco.subway.admin.exception.NotFoundPathException; +import wooteco.subway.admin.exception.NotFoundStationException; + +@ExtendWith(MockitoExtension.class) +class PathServiceTest { + private static final String STATION_NAME1 = "잠실역"; + private static final String STATION_NAME2 = "잠실새내역"; + private static final String STATION_NAME3 = "종합운동장역"; + private static final String STATION_NAME4 = "삼전역"; + private static final String STATION_NAME5 = "석촌고분역"; + private static final String STATION_NAME6 = "석촌역"; + private static final String STATION_NAME7 = "부산역"; + private static final String STATION_NAME8 = "대구역"; + private static final String STATION_NAME9 = "런던역"; + + @Mock + private StationService stationService; + + @Mock + private LineService lineService; + + private PathAlgorithm pathAlgorithm = new PathAlgorithmByDijkstra(); + + private PathService pathService; + + private Line line1; + private Line line2; + private Line line3; + private Line line4; + private Station station1; + private Station station2; + private Station station3; + private Station station4; + private Station station5; + private Station station6; + private Station station7; + private Station station8; + private Station station9; + + private List lines; + private List stations; + + @BeforeEach + void setUp() { + pathService = new PathService(stationService, lineService, pathAlgorithm); + + station1 = new Station(1L, STATION_NAME1); + station2 = new Station(2L, STATION_NAME2); + station3 = new Station(3L, STATION_NAME3); + station4 = new Station(4L, STATION_NAME4); + station5 = new Station(5L, STATION_NAME5); + station6 = new Station(6L, STATION_NAME6); + station7 = new Station(7L, STATION_NAME7); + station8 = new Station(8L, STATION_NAME8); + station9 = new Station(9L, STATION_NAME8); + + line1 = Line.of(1L, "2호선", "bg-green-400", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); + line1.addLineStation(new LineStation(null, 1L, 0, 0)); + line1.addLineStation(new LineStation(1L, 2L, 10, 1)); + line1.addLineStation(new LineStation(2L, 3L, 10, 1)); + line1.addLineStation(new LineStation(3L, 4L, 10, 1)); + + line2 = Line.of(2L, "8호선", "bg-pink-600", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); + line2.addLineStation(new LineStation(null, 1L, 1, 10)); + line2.addLineStation(new LineStation(1L, 6L, 1, 10)); + + line3 = Line.of(3L, "9호선", "bg-yellow-700", LocalTime.of(05, 30), LocalTime.of(22, 30), 5); + line3.addLineStation(new LineStation(null, 3L, 0, 0)); + line3.addLineStation(new LineStation(3L, 4L, 1, 10)); + line3.addLineStation(new LineStation(4L, 5L, 1, 10)); + line3.addLineStation(new LineStation(5L, 6L, 1, 10)); + + line4 = Line.of(4L, "대구1호선", "bg-indigo-500", LocalTime.of(05, 30), LocalTime.of(22, 30), + 5); + line4.addLineStation(new LineStation(null, 7L, 0, 0)); + line4.addLineStation(new LineStation(7L, 8L, 20, 20)); + + stations = Arrays.asList(station1, station2, station3, station4, station5, station6, + station7, station8); + lines = Arrays.asList(line1, line2, line3, line4); + } + + @Test + void findPathForNextStation() { + PathRequest request = new PathRequest(station1.getName(), + station2.getName(), PathType.DISTANCE.name()); + + when(stationService.findIdByName(station1.getName())).thenReturn(station1.getId()); + when(stationService.findIdByName(station2.getName())).thenReturn(station2.getId()); + when(lineService.findAll()).thenReturn(lines); + when(stationService.findAllById(anyList())).thenReturn(Arrays.asList(station1, station2)); + PathResponse path = pathService.findPath(request); + + assertThat(path.getStations()).extracting(StationResponse::getName) + .containsExactly(station1.getName(), + station2.getName()); + assertThat(path.getTotalDistance()).isEqualTo(10); + assertThat(path.getTotalDuration()).isEqualTo(1); + } + + @Test + void findPathForNotConnectedStation() { + PathRequest request = new PathRequest(station1.getName(), + station8.getName(), PathType.DISTANCE.name()); + + when(stationService.findIdByName(station1.getName())).thenReturn(station1.getId()); + when(stationService.findIdByName(station8.getName())).thenReturn(station8.getId()); + when(lineService.findAll()).thenReturn(lines); + assertThatThrownBy(() -> pathService.findPath(request)) + .isInstanceOf(NotFoundPathException.class); + } + + @Test + void findPathForSameStation() { + PathRequest request = new PathRequest(station1.getName(), + station1.getName(), PathType.DISTANCE.name()); + + when(lineService.findAll()).thenReturn(lines); + when(stationService.findIdByName(anyString())).thenReturn(station1.getId()); + assertThatThrownBy(() -> pathService.findPath(request)) + .isInstanceOf(IllegalStationNameException.class); + } + + @Test + void findPathForNotExistStation() { + PathRequest request = new PathRequest(station1.getName(), station9.getName(), + PathType.DISTANCE.name()); + + when(stationService.findIdByName(station1.getName())).thenReturn(station1.getId()); + when(stationService.findIdByName(station9.getName())) + .thenThrow(new NotFoundStationException()); + assertThatThrownBy(() -> pathService.findPath(request)) + .isInstanceOf(NotFoundStationException.class); + } + + @Test + void findPathForNotExistLines() { + PathRequest request = new PathRequest(station1.getName(), station2.getName(), + PathType.DISTANCE.name()); + when(stationService.findIdByName(station1.getName())).thenReturn(station1.getId()); + when(stationService.findIdByName(station2.getName())).thenReturn(station2.getId()); + when(lineService.findAll()).thenReturn(null); + assertThatThrownBy(() -> pathService.findPath(request)).isInstanceOf( + NotFoundLineException.class); + } +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 000000000..cb6272877 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,2 @@ +spring.datasource.data= +handlebars.suffix=.html \ No newline at end of file