diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..2551edaa2 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,109 @@ +# 지하철 노선도 경로 조회 미션 + +*작성자: 황진성(eddy5360@naver.com)* + +우아한 테크코스 3기 웹 백엔드 선발 과정 최종 코딩테스트입니다. + +
+ +## 📃 구현할 기능 목록 + +
+ +* 초기 설정 +``` +- 노선, 역, 거리, 시간 정보들을 초기화한다. +- 방향이 정해져있지 않으므로 경로를 양방향으로 설정한다. +``` + +* 경로 조회 기능 +``` +- 출발역과 도착역을 입력받는다. +- 최단거리 또는 최소시간 기준으로 경로를 조회할 수 있다. +[예외상황] + - 출발역과 도착역이 동일하다. + - 출발역에서 도착역으로 갈 수 없다(연결되어있지 않다.) +``` + +
+ +## ✍ 클래스 설계 목록 + +
+ +## domain + +* Line +``` +- 노선의 이름을 저장한다. +``` + +* LineRepository +``` +- 노선들의 목록을 저장한다. +``` + +* Station +``` +- 역의 이름을 저장한다. +``` + +* StationRepository +``` +- 역들의 목록을 저장한다. +``` + +* Connection +``` +- 연결된 역 사이의 시간과 거리를 저장한다. +``` + +* ConnectionRepository +``` +- 모든 Connection 객체들을 일급 컬렉션 형태로 저장한다. +``` + + +## view + +* InputView +``` +- 기능 선택 +- 출발, 도착 역 입력 +``` + +* OutputView +``` +- 메인 화면 출력 + - 1. 경로 조회 + - Q. 종료 +- 경로 조회 화면 출력 + - 1. 최단거리 + - 2. 최소시간 + - B. 돌아가기 +- 조회 결과 출력 +``` + + +## controller + +* MainController +``` +- 메인 화면을 출력하고, 입력받은 기능 값에 의존해 다음 기능을 수행한다. +``` + +* InquirePathController +``` +- 경로 기준 화면을 출력하고, 입력받은 기능 값에 의존해 경로를 조회한다. +``` + + +## util + +* Validator +``` +- 현재 화면에서 선택할 수 있는 기능인지 검증한다. +- 출발역과 도착역이 같은지 검증한다. +- 존재하는 역을 입력했는지 검증한다. +- 서로 연결된 역인지 검증한다. +``` \ No newline at end of file diff --git a/src/main/java/subway/Application.java b/src/main/java/subway/Application.java index 0bcf786cc..7494ec7d2 100644 --- a/src/main/java/subway/Application.java +++ b/src/main/java/subway/Application.java @@ -1,10 +1,19 @@ package subway; +import subway.controller.MainController; +import subway.util.Initialization; +import subway.view.InputView; + import java.util.Scanner; public class Application { public static void main(String[] args) { final Scanner scanner = new Scanner(System.in); - // TODO: 프로그램 구현 + Initialization.set(); + InputView inputView = new InputView(scanner); + MainController mainController = new MainController(inputView); + mainController.run(); + + scanner.close(); } } diff --git a/src/main/java/subway/controller/MainButton.java b/src/main/java/subway/controller/MainButton.java new file mode 100644 index 000000000..be981d38e --- /dev/null +++ b/src/main/java/subway/controller/MainButton.java @@ -0,0 +1,15 @@ +package subway.controller; + +public enum MainButton { + INQUIRY("1"), EXIT("Q"); + + private String symbol; + + MainButton(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } +} diff --git a/src/main/java/subway/controller/MainController.java b/src/main/java/subway/controller/MainController.java new file mode 100644 index 000000000..78aa09e07 --- /dev/null +++ b/src/main/java/subway/controller/MainController.java @@ -0,0 +1,37 @@ +package subway.controller; + +import subway.view.InputView; +import subway.view.OutputView; + +import java.util.Arrays; +import java.util.List; + +public class MainController { + + private final InputView inputView; + private final PathController pathController; + + public MainController(InputView inputView) { + this.inputView = inputView; + pathController = new PathController(inputView); + } + + private final List buttons = Arrays.asList( + MainButton.INQUIRY.getSymbol(), + MainButton.EXIT.getSymbol() + ); + + public void run() { + OutputView.printMain(); + String selectedButton = inputView.getFunctionSelect(buttons); + nextProcedure(selectedButton); + } + + private void nextProcedure(final String button) { + if (button.equals(MainButton.INQUIRY.getSymbol())) { + pathController.run(); + run(); + } + } + +} diff --git a/src/main/java/subway/controller/PathButton.java b/src/main/java/subway/controller/PathButton.java new file mode 100644 index 000000000..c808de410 --- /dev/null +++ b/src/main/java/subway/controller/PathButton.java @@ -0,0 +1,15 @@ +package subway.controller; + +public enum PathButton { + SHORTEST_PATH("1"), LEAST_TIME("2"), BACK("B"); + + private String symbol; + + PathButton(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } +} diff --git a/src/main/java/subway/controller/PathController.java b/src/main/java/subway/controller/PathController.java new file mode 100644 index 000000000..4b9887f11 --- /dev/null +++ b/src/main/java/subway/controller/PathController.java @@ -0,0 +1,54 @@ +package subway.controller; + +import subway.graph.DistanceWeightedGraph; +import subway.graph.TimeWeightedGraph; +import subway.util.Validator; +import subway.view.InputView; +import subway.view.OutputView; + +import java.util.Arrays; +import java.util.List; + +public class PathController { + + private final InputView inputView; + + public PathController(InputView inputView) { + this.inputView = inputView; + } + + private final List buttons = Arrays.asList( + PathButton.SHORTEST_PATH.getSymbol(), + PathButton.LEAST_TIME.getSymbol(), + PathButton.BACK.getSymbol() + ); + + public void run() { + OutputView.printInquiry(); + String selectedButton = inputView.getFunctionSelect(buttons); + nextProcedure(selectedButton); + } + + private void nextProcedure(final String button) { + if (button.equals(PathButton.BACK.getSymbol())) { + return; + } + String source = inputView.getSourceStation(); + String destination = inputView.getDestinationStation(); + if (Validator.sameStation(source, destination)) { + nextProcedure(button); + return; + } + if (Validator.unconnected(source, destination)) { + nextProcedure(button); + return; + } + // 총 시간과 거리를 계산하는 함수를 구현하지 못했습니다. + if (button.equals(PathButton.SHORTEST_PATH.getSymbol())) { + OutputView.printInquiryGraph(DistanceWeightedGraph.getOptimalGraph(source, destination), -1, -1); + } else if (button.equals(PathButton.LEAST_TIME.getSymbol())) { + OutputView.printInquiryGraph(TimeWeightedGraph.getOptimalGraph(source, destination), -1, -1); + } + } + +} diff --git a/src/main/java/subway/domain/Connection.java b/src/main/java/subway/domain/Connection.java new file mode 100644 index 000000000..c15c461c2 --- /dev/null +++ b/src/main/java/subway/domain/Connection.java @@ -0,0 +1,36 @@ +package subway.domain; + +public class Connection { + + private final String source; + private final String destination; + private final int distance; // (km) 단위 + private final int time; // (분) 단위 + + public Connection(final String source, final String destination, final int distance, final int time) { + this.source = source; + this.destination = destination; + this.distance = distance; + this.time = time; + } + + public Connection getReverse() { + return new Connection(destination, source, distance, time); + } + + public String getSource() { + return source; + } + + public String getDestination() { + return destination; + } + + public int getDistance() { + return distance; + } + + public int getTime() { + return time; + } +} diff --git a/src/main/java/subway/domain/ConnectionRepository.java b/src/main/java/subway/domain/ConnectionRepository.java new file mode 100644 index 000000000..cbe065d9c --- /dev/null +++ b/src/main/java/subway/domain/ConnectionRepository.java @@ -0,0 +1,18 @@ +package subway.domain; + +import java.util.ArrayList; +import java.util.List; + +public class ConnectionRepository { + + private static List connections = new ArrayList<>(); + + public static void addConnection(Connection connection) { + connections.add(connection); + connections.add(connection.getReverse()); + } + + public static List connections() { + return connections; + } +} diff --git a/src/main/java/subway/domain/StationRepository.java b/src/main/java/subway/domain/StationRepository.java index 8ed9d103f..a6dd579dd 100644 --- a/src/main/java/subway/domain/StationRepository.java +++ b/src/main/java/subway/domain/StationRepository.java @@ -23,4 +23,14 @@ public static boolean deleteStation(String name) { public static void deleteAll() { stations.clear(); } + + public static boolean isExist(String stationName) { + for (Station station : stations) { + if (station.getName().equals(stationName)) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/subway/graph/DistanceWeightedGraph.java b/src/main/java/subway/graph/DistanceWeightedGraph.java new file mode 100644 index 000000000..23f471b89 --- /dev/null +++ b/src/main/java/subway/graph/DistanceWeightedGraph.java @@ -0,0 +1,31 @@ +package subway.graph; + +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; + +import java.util.List; + +public class DistanceWeightedGraph { + + private static final WeightedMultigraph graph = new WeightedMultigraph(DefaultWeightedEdge.class); + + public static void addVertex(String vertex) { + graph.addVertex(vertex); + } + + public static DefaultWeightedEdge addEdge(String source, String destination) { + return graph.addEdge(source, destination); + } + + public static void setEdgeWeight(DefaultWeightedEdge DWE, double weight) { + graph.setEdgeWeight(DWE, weight); + } + + public static List getOptimalGraph(String source, String destination) { + DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); + List optimalGraph = dijkstraShortestPath.getPath(source, destination).getVertexList(); + return optimalGraph; + } + +} diff --git a/src/main/java/subway/graph/TimeWeightedGraph.java b/src/main/java/subway/graph/TimeWeightedGraph.java new file mode 100644 index 000000000..78ffd9d31 --- /dev/null +++ b/src/main/java/subway/graph/TimeWeightedGraph.java @@ -0,0 +1,31 @@ +package subway.graph; + +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; + +import java.util.List; + +public class TimeWeightedGraph { + + private static final WeightedMultigraph graph = new WeightedMultigraph(DefaultWeightedEdge.class); + + public static void addVertex(String vertex) { + graph.addVertex(vertex); + } + + public static DefaultWeightedEdge addEdge(String source, String destination) { + return graph.addEdge(source, destination); + } + + public static void setEdgeWeight(DefaultWeightedEdge DWE, double weight) { + graph.setEdgeWeight(DWE, weight); + } + + public static List getOptimalGraph(String source, String destination) { + DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); + List optimalGraph = dijkstraShortestPath.getPath(source, destination).getVertexList(); + return optimalGraph; + } + +} diff --git a/src/main/java/subway/legacy/Lines.java b/src/main/java/subway/legacy/Lines.java new file mode 100644 index 000000000..71bb4a687 --- /dev/null +++ b/src/main/java/subway/legacy/Lines.java @@ -0,0 +1,17 @@ +package subway.legacy; + +public enum Lines { + + LINE_2("2호선"), + LINE_3("3호선"), + LINE_SHINBUNDANG("신분당선"); + + private String name; + Lines(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/subway/legacy/Stations.java b/src/main/java/subway/legacy/Stations.java new file mode 100644 index 000000000..83002f195 --- /dev/null +++ b/src/main/java/subway/legacy/Stations.java @@ -0,0 +1,22 @@ +package subway.legacy; + +public enum Stations { + + GYODAE("교대역"), + GANGNAM("강남역"), + YEOKSAM("역삼역"), + NAMBU_BUS_TERMINAL("남부터미널역"), + YANGJAE("양재역"), + YANGJAE_CITIZEN_FOREST("양재시민의숲역"), + MAEBONG("매봉역"); + + private String name; + Stations(String name){ + this.name = name; + } + + public String getName() { + return name; + } + +} diff --git a/src/main/java/subway/util/Initialization.java b/src/main/java/subway/util/Initialization.java new file mode 100644 index 000000000..4139b8e3a --- /dev/null +++ b/src/main/java/subway/util/Initialization.java @@ -0,0 +1,55 @@ +package subway.util; + +import subway.graph.DistanceWeightedGraph; +import subway.graph.TimeWeightedGraph; +import subway.legacy.Lines; +import subway.legacy.Stations; +import subway.domain.*; + +public class Initialization { + + public static void set() { + registerStations(); + registerLines(); + registerConnections(); + setGraph(); + } + + private static void registerStations() { + StationRepository.addStation(new Station(Stations.GYODAE.getName())); + StationRepository.addStation(new Station(Stations.GANGNAM.getName())); + StationRepository.addStation(new Station(Stations.YEOKSAM.getName())); + StationRepository.addStation(new Station(Stations.NAMBU_BUS_TERMINAL.getName())); + StationRepository.addStation(new Station(Stations.YANGJAE.getName())); + StationRepository.addStation(new Station(Stations.YANGJAE_CITIZEN_FOREST.getName())); + StationRepository.addStation(new Station(Stations.MAEBONG.getName())); + } + + private static void registerLines() { + LineRepository.addLine(new Line(Lines.LINE_2.getName())); + LineRepository.addLine(new Line(Lines.LINE_3.getName())); + LineRepository.addLine(new Line(Lines.LINE_SHINBUNDANG.getName())); + } + + private static void registerConnections() { + ConnectionRepository.addConnection(new Connection(Stations.GYODAE.getName(), Stations.GANGNAM.getName(), 2, 3)); + ConnectionRepository.addConnection(new Connection(Stations.GANGNAM.getName(), Stations.YEOKSAM.getName(), 2, 3)); + ConnectionRepository.addConnection(new Connection(Stations.GYODAE.getName(), Stations.NAMBU_BUS_TERMINAL.getName(), 3,2)); + ConnectionRepository.addConnection(new Connection(Stations.NAMBU_BUS_TERMINAL.getName(), Stations.YANGJAE.getName(), 6,5)); + ConnectionRepository.addConnection(new Connection(Stations.YANGJAE.getName(), Stations.MAEBONG.getName(), 1,1)); + ConnectionRepository.addConnection(new Connection(Stations.GANGNAM.getName(), Stations.YANGJAE.getName(), 2,8)); + ConnectionRepository.addConnection(new Connection(Stations.YANGJAE.getName(), Stations.YANGJAE_CITIZEN_FOREST.getName(), 10,3)); + } + + private static void setGraph() { + for (Station station : StationRepository.stations()) { + DistanceWeightedGraph.addVertex(station.getName()); + TimeWeightedGraph.addVertex(station.getName()); + } + for (Connection connection : ConnectionRepository.connections()) { + DistanceWeightedGraph.setEdgeWeight(DistanceWeightedGraph.addEdge(connection.getSource(), connection.getDestination()), connection.getDistance()); + TimeWeightedGraph.setEdgeWeight(TimeWeightedGraph.addEdge(connection.getSource(), connection.getDestination()), connection.getTime()); + } + } + +} \ No newline at end of file diff --git a/src/main/java/subway/util/Validator.java b/src/main/java/subway/util/Validator.java new file mode 100644 index 000000000..f92c52ba8 --- /dev/null +++ b/src/main/java/subway/util/Validator.java @@ -0,0 +1,37 @@ +package subway.util; + +import subway.domain.StationRepository; +import subway.view.OutputView; + +import java.util.List; + +public class Validator { + + public static void functionSelect(final List choices, final String command) { + if (!choices.contains(command)) { + OutputView.printError(OutputView.ERROR_INVALID_SELECT); + throw new IllegalArgumentException(); + } + } + + public static void existStation(final String stationName) { + if (!StationRepository.isExist(stationName)){ + OutputView.printError(OutputView.ERROR_INVALID_STATION); + throw new IllegalArgumentException(); + } + } + + public static boolean sameStation(final String source, final String destination) { + if (source.equals(destination)) { + OutputView.printError(OutputView.ERROR_SAME_STATION); + return true; + } + return false; + } + + public static boolean unconnected(final String source, final String destination) { + // 구현 못했습니다. + return false; + } + +} diff --git a/src/main/java/subway/view/InputView.java b/src/main/java/subway/view/InputView.java new file mode 100644 index 000000000..343d04a18 --- /dev/null +++ b/src/main/java/subway/view/InputView.java @@ -0,0 +1,51 @@ +package subway.view; + +import subway.util.Validator; + +import java.util.List; +import java.util.Scanner; + +public class InputView { + + private final Scanner scanner; + + public InputView(Scanner scanner) { + this.scanner = scanner; + } + + public String getFunctionSelect(List buttons) { + OutputView.printFunctionSelectQuery(); + try { + String input = scanner.nextLine(); + System.out.println(); + Validator.functionSelect(buttons, input); + return input; + } catch (IllegalArgumentException IAE) { + return getFunctionSelect(buttons); + } + } + + public String getSourceStation() { + OutputView.printSourceStationQuery(); + try { + String input = scanner.nextLine(); + OutputView.printEmptyLine(); + Validator.existStation(input); + return input; + } catch (IllegalArgumentException IAE) { + return getSourceStation(); + } + } + + public String getDestinationStation() { + OutputView.printDestinationStationQuery(); + try { + String input = scanner.nextLine(); + OutputView.printEmptyLine(); + Validator.existStation(input); + return input; + } catch (IllegalArgumentException IAE) { + return getDestinationStation(); + } + } +} diff --git a/src/main/java/subway/view/OutputView.java b/src/main/java/subway/view/OutputView.java new file mode 100644 index 000000000..0ec91a0c7 --- /dev/null +++ b/src/main/java/subway/view/OutputView.java @@ -0,0 +1,83 @@ +package subway.view; + +import java.util.List; + +public class OutputView { + + private static final String MAIN_TITLE = "## 메인 화면"; + private static final String MAIN_OPTION_INQUIRY = "1. 경로 조회"; + private static final String MAIN_OPTION_EXIT = "Q. 종료"; + + private static final String INQUIRY_TITLE = "## 경로 기준"; + private static final String INQUIRY_SHORTEST_PATH = "1. 최단 거리"; + private static final String INQUIRY_LEAST_TIME = "2. 최소 시간"; + private static final String INQUIRY_BACK = "B. 돌아가기"; + + private static final String RESULT_TITLE = "## 조회 결과"; + private static final String RESULT_LINE = "---"; + private static final String RESULT_DISTANCE_FORMAT = "총 거리: %dkm"; + private static final String RESULT_TIME_FORMAT = "총 소요 시간: %d분"; + + private static final String QUERY_FUNCTION_SELECT = "## 원하는 기능을 선택하세요."; + private static final String QUERY_SOURCE_STATION = "## 출발역을 입력하세요."; + private static final String QUERY_DESTINATION_STATION = "## 도착역을 입력하세요."; + + private static final String TRY_AGAIN = " 다시 입력해주세요."; + public static final String ERROR_INVALID_SELECT = "선택할 수 없는 기능입니다." + TRY_AGAIN; + public static final String ERROR_INVALID_STATION = "존재하지 않는 역입니다." + TRY_AGAIN; + public static final String ERROR_SAME_STATION = "출발역과 도착역이 동일합니다." + TRY_AGAIN; + public static final String ERROR_UNREACHABLE = "죄송합니다. 두 역은 연결되어 있지 않습니다."; + + public static void printMain() { + System.out.println(MAIN_TITLE); + System.out.println(MAIN_OPTION_INQUIRY); + System.out.println(MAIN_OPTION_EXIT); + printEmptyLine(); + } + + public static void printInquiry() { + System.out.println(INQUIRY_TITLE); + System.out.println(INQUIRY_SHORTEST_PATH); + System.out.println(INQUIRY_LEAST_TIME); + System.out.println(INQUIRY_BACK); + printEmptyLine(); + } + + public static void printFunctionSelectQuery() { + System.out.println(QUERY_FUNCTION_SELECT); + } + + public static void printSourceStationQuery() { + System.out.println(QUERY_SOURCE_STATION); + } + + public static void printDestinationStationQuery() { + System.out.println(QUERY_DESTINATION_STATION); + } + + public static void printInquiryGraph(final List graph, final int totalDistance, final int totalTime) { + System.out.println(RESULT_TITLE); + printInformation(RESULT_LINE); + printInformation(String.format(RESULT_DISTANCE_FORMAT, totalDistance)); + printInformation(String.format(RESULT_TIME_FORMAT, totalTime)); + printInformation(RESULT_LINE); + for (String stationName : graph) { + printInformation(stationName); + } + printEmptyLine(); + } + + public static void printInformation(final String message) { + System.out.println("[INFO] " + message); + } + + public static void printError(final String message) { + System.out.println("[ERROR] " + message); + printEmptyLine(); + } + + public static void printEmptyLine() { + System.out.println(); + } + +}