From 488e0639315ddf8ea6bbf65674bcc77e962e4ae7 Mon Sep 17 00:00:00 2001 From: gmelon Date: Sat, 10 Jun 2023 17:03:37 +0900 Subject: [PATCH 1/9] =?UTF-8?q?Step2=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/subway/README.md diff --git a/src/main/java/subway/README.md b/src/main/java/subway/README.md new file mode 100644 index 00000000..d9e2a621 --- /dev/null +++ b/src/main/java/subway/README.md @@ -0,0 +1,14 @@ +## 2단계 리팩토링 요구사항 +- [ ] SectionService 부정형 조건문 !(a || b) 형태로 바꾸기 +- [ ] Station에서 id를 꺼내 외부에서 비교하지 말고 직접 물어보기 + - SectionService 개선 +- [ ] Section 도메인 생성자 체이닝 적용 +- [ ] test에서 필드끼리 비교할 때 usingRecursiveComparison() 사용해보기 +- [ ] LineDaoTest - hasSize(0) 을 isEmpty() 로 개선 + +## 2단계 기능 요구사항 +- [ ] 프로덕션, 테스트용 profile 다르게 설정하기 + - 프로덕션 DB는 로컬에 저장, 테스트 DB는 인메모리로 동작하도록 설정 +- [ ] 경로 조회 API 구현 + - 1. 최단 거리 경로, 2. 거리 정보, 3. 해당 거리에 대한 요금 응답 + - 다른 노선으로의 환승도 고려해야 함 From 786055178c86b5f230a5bf066f3066f42cd0edf2 Mon Sep 17 00:00:00 2001 From: gmelon Date: Sat, 10 Jun 2023 17:05:21 +0900 Subject: [PATCH 2/9] =?UTF-8?q?refactor:=20SectionService=20=EB=B6=80?= =?UTF-8?q?=EC=A0=95=ED=98=95=20=EC=A1=B0=EA=B1=B4=EB=AC=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/README.md | 2 +- src/main/java/subway/application/SectionService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/subway/README.md b/src/main/java/subway/README.md index d9e2a621..09b37b35 100644 --- a/src/main/java/subway/README.md +++ b/src/main/java/subway/README.md @@ -1,5 +1,5 @@ ## 2단계 리팩토링 요구사항 -- [ ] SectionService 부정형 조건문 !(a || b) 형태로 바꾸기 +- [x] SectionService 부정형 조건문 !(a || b) 형태로 바꾸기 - [ ] Station에서 id를 꺼내 외부에서 비교하지 말고 직접 물어보기 - SectionService 개선 - [ ] Section 도메인 생성자 체이닝 적용 diff --git a/src/main/java/subway/application/SectionService.java b/src/main/java/subway/application/SectionService.java index 5530eafd..1f5e4649 100644 --- a/src/main/java/subway/application/SectionService.java +++ b/src/main/java/subway/application/SectionService.java @@ -59,7 +59,7 @@ private boolean isLineEmpty(Long lineId) { } private boolean isUpStationDoesNotEqualToLastDownStation(List
sections, List distinctStations, Long upStationId) { - return !sections.isEmpty() && !getLastStationId(distinctStations).equals(upStationId); + return !(sections.isEmpty() || getLastStationId(distinctStations).equals(upStationId)); } private Long getLastStationId(List stations) { From 53ebcd56450dbf304a35c87ab3b8a1ac987fdf42 Mon Sep 17 00:00:00 2001 From: gmelon Date: Sat, 10 Jun 2023 17:17:54 +0900 Subject: [PATCH 3/9] =?UTF-8?q?refactor:=20Station,=20Section=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9D=B4=20=EC=A7=81=EC=A0=91=20id=EB=A5=BC?= =?UTF-8?q?=20=EB=B9=84=EA=B5=90=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/README.md | 2 +- .../java/subway/application/SectionService.java | 15 +++++++-------- src/main/java/subway/domain/Section.java | 4 ++++ src/main/java/subway/domain/Station.java | 4 ++++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/subway/README.md b/src/main/java/subway/README.md index 09b37b35..b6afa15c 100644 --- a/src/main/java/subway/README.md +++ b/src/main/java/subway/README.md @@ -1,6 +1,6 @@ ## 2단계 리팩토링 요구사항 - [x] SectionService 부정형 조건문 !(a || b) 형태로 바꾸기 -- [ ] Station에서 id를 꺼내 외부에서 비교하지 말고 직접 물어보기 +- [x] Station에서 id를 꺼내 외부에서 비교하지 말고 직접 물어보기 - SectionService 개선 - [ ] Section 도메인 생성자 체이닝 적용 - [ ] test에서 필드끼리 비교할 때 usingRecursiveComparison() 사용해보기 diff --git a/src/main/java/subway/application/SectionService.java b/src/main/java/subway/application/SectionService.java index 1f5e4649..9926157e 100644 --- a/src/main/java/subway/application/SectionService.java +++ b/src/main/java/subway/application/SectionService.java @@ -59,17 +59,16 @@ private boolean isLineEmpty(Long lineId) { } private boolean isUpStationDoesNotEqualToLastDownStation(List
sections, List distinctStations, Long upStationId) { - return !(sections.isEmpty() || getLastStationId(distinctStations).equals(upStationId)); + return !(sections.isEmpty() || getLastStation(distinctStations).isIdEquals(upStationId)); } - private Long getLastStationId(List stations) { - return stations.get(stations.size() - 1).getId(); + private Station getLastStation(List stations) { + return stations.get(stations.size() - 1); } private boolean isStationAlreadyExists(List distinctStations, Long downStationId) { return distinctStations.stream() - .map(Station::getId) - .anyMatch(id -> id.equals(downStationId)); + .anyMatch(station -> station.isIdEquals(downStationId)); } public void deleteSectionById(Long lineId, Long sectionId) { @@ -86,10 +85,10 @@ private void validateDeleteRequest(Long lineId, Long sectionId) { } private boolean isNotLastSection(List
sections, Long sectionId) { - return !sections.isEmpty() && !getLastSectionId(sections).equals(sectionId); + return !(sections.isEmpty() || getLastSection(sections).isIdEquals(sectionId)); } - private Long getLastSectionId(List
sections) { - return sections.get(sections.size() - 1).getId(); + private Section getLastSection(List
sections) { + return sections.get(sections.size() - 1); } } diff --git a/src/main/java/subway/domain/Section.java b/src/main/java/subway/domain/Section.java index 60d38586..203c175f 100644 --- a/src/main/java/subway/domain/Section.java +++ b/src/main/java/subway/domain/Section.java @@ -41,6 +41,10 @@ public Integer getDistance() { return distance; } + public boolean isIdEquals(Long id) { + return this.id.equals(id); + } + public static List distinctStations(List
sections) { return sections.stream() .flatMap(section -> Stream.of(section.getUpStation(), section.getDownStation())) diff --git a/src/main/java/subway/domain/Station.java b/src/main/java/subway/domain/Station.java index dbf9d783..ed6dd7b2 100644 --- a/src/main/java/subway/domain/Station.java +++ b/src/main/java/subway/domain/Station.java @@ -26,6 +26,10 @@ public String getName() { return name; } + public boolean isIdEquals(Long id) { + return this.id.equals(id); + } + @Override public boolean equals(Object o) { if (this == o) return true; From d10366170d7cfe1374b870676462b6b9a95311ca Mon Sep 17 00:00:00 2001 From: gmelon Date: Sat, 10 Jun 2023 17:18:49 +0900 Subject: [PATCH 4/9] =?UTF-8?q?refactor:=20Section=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=B2=B4=EC=9D=B4?= =?UTF-8?q?=EB=8B=9D=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/README.md | 2 +- src/main/java/subway/domain/Section.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/subway/README.md b/src/main/java/subway/README.md index b6afa15c..9890dd0f 100644 --- a/src/main/java/subway/README.md +++ b/src/main/java/subway/README.md @@ -2,7 +2,7 @@ - [x] SectionService 부정형 조건문 !(a || b) 형태로 바꾸기 - [x] Station에서 id를 꺼내 외부에서 비교하지 말고 직접 물어보기 - SectionService 개선 -- [ ] Section 도메인 생성자 체이닝 적용 +- [x] Section 도메인 생성자 체이닝 적용 - [ ] test에서 필드끼리 비교할 때 usingRecursiveComparison() 사용해보기 - [ ] LineDaoTest - hasSize(0) 을 isEmpty() 로 개선 diff --git a/src/main/java/subway/domain/Section.java b/src/main/java/subway/domain/Section.java index 203c175f..c5ece016 100644 --- a/src/main/java/subway/domain/Section.java +++ b/src/main/java/subway/domain/Section.java @@ -13,9 +13,7 @@ public class Section { private Integer distance; public Section(Station upStation, Station downStation, Integer distance) { - this.upStation = upStation; - this.downStation = downStation; - this.distance = distance; + this(null, upStation, downStation, distance); } public Section(Long id, Station upStation, Station downStation, Integer distance) { From 5e64bfa627d066f6e3b188c16017f4aab28d8f6b Mon Sep 17 00:00:00 2001 From: gmelon Date: Sun, 11 Jun 2023 13:53:43 +0900 Subject: [PATCH 5/9] =?UTF-8?q?test:=20=EB=8F=99=EC=9D=BC=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EC=9D=98=20=ED=95=84=EB=93=9C=EB=81=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=ED=95=A0=20=EB=95=8C=20usingRecursiveCompari?= =?UTF-8?q?son()=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test: 동일 객체의 필드끼리 비교할 때 usingRecursiveComparison() 사용하도록 개선 --- src/main/java/subway/README.md | 4 ++-- .../subway/application/LineServiceTest.java | 2 +- src/test/java/subway/dao/LineDaoTest.java | 20 +++++++++++-------- src/test/java/subway/dao/StationDaoTest.java | 20 ++++++++++++------- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/main/java/subway/README.md b/src/main/java/subway/README.md index 9890dd0f..de0d389a 100644 --- a/src/main/java/subway/README.md +++ b/src/main/java/subway/README.md @@ -3,8 +3,8 @@ - [x] Station에서 id를 꺼내 외부에서 비교하지 말고 직접 물어보기 - SectionService 개선 - [x] Section 도메인 생성자 체이닝 적용 -- [ ] test에서 필드끼리 비교할 때 usingRecursiveComparison() 사용해보기 -- [ ] LineDaoTest - hasSize(0) 을 isEmpty() 로 개선 +- [x] test에서 필드끼리 비교할 때 usingRecursiveComparison() 사용해보기 +- [x] LineDaoTest - hasSize(0) 을 isEmpty() 로 개선 ## 2단계 기능 요구사항 - [ ] 프로덕션, 테스트용 profile 다르게 설정하기 diff --git a/src/test/java/subway/application/LineServiceTest.java b/src/test/java/subway/application/LineServiceTest.java index cf426c3a..4d11188c 100644 --- a/src/test/java/subway/application/LineServiceTest.java +++ b/src/test/java/subway/application/LineServiceTest.java @@ -13,7 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @DisplayName("Line 서비스 로직") @SpringBootTest diff --git a/src/test/java/subway/dao/LineDaoTest.java b/src/test/java/subway/dao/LineDaoTest.java index f3ac00c7..a1197b3c 100644 --- a/src/test/java/subway/dao/LineDaoTest.java +++ b/src/test/java/subway/dao/LineDaoTest.java @@ -49,9 +49,10 @@ void insert() { // then assertThat(insertedLine.getId()).isNotNull(); - assertThat(insertedLine.getName()).isEqualTo(LINE_1.getName()); - assertThat(insertedLine.getColor()).isEqualTo(LINE_1.getColor()); - assertThat(insertedLine.getSections()).hasSize(0); + assertThat(insertedLine) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(LINE_1); } @DisplayName("Line 테이블에서 모든 엔티티를 조회한다") @@ -84,9 +85,10 @@ public void findById() { // then Line foundLine = assertDoesNotThrow(() -> foundOptionalLine.get()); - assertThat(foundLine.getId()).isNotNull(); - assertThat(foundLine.getName()).isEqualTo(LINE_1.getName()); - assertThat(foundLine.getColor()).isEqualTo(LINE_1.getColor()); + assertThat(foundLine) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(LINE_1); } @DisplayName("Line id로 section 리스트를 갖는 Line 테이블의 엔티티를 조회한다") @@ -117,8 +119,10 @@ public void update() { // then Line updatedLine = lineDao.findById(insertedLine.getId()).get(); - assertThat(updatedLine.getName()).isEqualTo(LINE_2.getName()); - assertThat(updatedLine.getColor()).isEqualTo(LINE_2.getColor()); + assertThat(updatedLine) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(LINE_2); } @DisplayName("Line 테이블의 엔티티를 삭제한다") diff --git a/src/test/java/subway/dao/StationDaoTest.java b/src/test/java/subway/dao/StationDaoTest.java index d3032e91..bdc0ce51 100644 --- a/src/test/java/subway/dao/StationDaoTest.java +++ b/src/test/java/subway/dao/StationDaoTest.java @@ -6,17 +6,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.jdbc.core.JdbcTemplate; -import subway.domain.Line; -import subway.domain.Section; import subway.domain.Station; import javax.sql.DataSource; - import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @JdbcTest class StationDaoTest { @@ -44,7 +41,10 @@ void insert() { // then assertThat(insertedStation.getId()).isNotNull(); - assertThat(insertedStation.getName()).isEqualTo(STATION_1.getName()); + assertThat(insertedStation) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(STATION_1); } @DisplayName("Station 테이블에서 모든 엔티티를 조회한다") @@ -75,7 +75,10 @@ public void findById() { // then Station foundStation = assertDoesNotThrow(() -> foundOptionalStation.get()); - assertThat(foundStation.getName()).isEqualTo(STATION_1.getName()); + assertThat(foundStation) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(STATION_1); } @DisplayName("Station 테이블의 엔티티를 수정한다") @@ -89,7 +92,10 @@ public void update() { // then Station updatedStation = stationDao.findById(insertedStation.getId()).get(); - assertThat(updatedStation.getName()).isEqualTo(STATION_2.getName()); + assertThat(updatedStation) + .usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(STATION_2); } @DisplayName("Station 테이블의 엔티티를 삭제한다") From b0637c1f2a39137a9c5ca27acb664943c6e77942 Mon Sep 17 00:00:00 2001 From: gmelon Date: Mon, 12 Jun 2023 21:34:32 +0900 Subject: [PATCH 6/9] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=9C=EB=8D=95?= =?UTF-8?q?=EC=85=98,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20profile=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/README.md | 2 +- src/main/resources/application.properties | 4 +++- src/test/resources/application.properties | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/application.properties diff --git a/src/main/java/subway/README.md b/src/main/java/subway/README.md index de0d389a..94bf97a4 100644 --- a/src/main/java/subway/README.md +++ b/src/main/java/subway/README.md @@ -7,7 +7,7 @@ - [x] LineDaoTest - hasSize(0) 을 isEmpty() 로 개선 ## 2단계 기능 요구사항 -- [ ] 프로덕션, 테스트용 profile 다르게 설정하기 +- [x] 프로덕션, 테스트용 profile 다르게 설정하기 - 프로덕션 DB는 로컬에 저장, 테스트 DB는 인메모리로 동작하도록 설정 - [ ] 경로 조회 API 구현 - 1. 최단 거리 경로, 2. 거리 정보, 3. 해당 거리에 대한 요금 응답 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a93b5921..3ef790fa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,6 @@ spring.h2.console.enabled=true -spring.datasource.url=jdbc:h2:mem:subway;MODE=MySQL +spring.datasource.url=jdbc:h2:~/subway;MODE=MySQL spring.datasource.driver-class-name=org.h2.Driver + +spring.sql.init.mode=always diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 00000000..bc14b829 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,2 @@ +spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL +spring.datasource.driver-class-name=org.h2.Driver From a25da118ea92e8e3250a58aa3af6741c72b89300 Mon Sep 17 00:00:00 2001 From: gmelon Date: Mon, 12 Jun 2023 23:41:04 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=EA=B2=BD=EB=A1=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Controller,=20ResponseDto,=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/application/PathService.java | 16 ++ src/main/java/subway/dto/PathResponse.java | 27 ++++ src/main/java/subway/ui/PathController.java | 25 +++ .../integration/PathIntegrationTest.java | 147 ++++++++++++++++++ src/test/resources/path-testdata.sql | 51 ++++++ 5 files changed, 266 insertions(+) create mode 100644 src/main/java/subway/application/PathService.java create mode 100644 src/main/java/subway/dto/PathResponse.java create mode 100644 src/main/java/subway/ui/PathController.java create mode 100644 src/test/java/subway/integration/PathIntegrationTest.java create mode 100644 src/test/resources/path-testdata.sql diff --git a/src/main/java/subway/application/PathService.java b/src/main/java/subway/application/PathService.java new file mode 100644 index 00000000..7a1fd0b5 --- /dev/null +++ b/src/main/java/subway/application/PathService.java @@ -0,0 +1,16 @@ +package subway.application; + +import org.springframework.stereotype.Service; +import subway.dto.PathResponse; + +import java.util.List; + +@Service +public class PathService { + + public PathResponse findShortestPath(Long departureStationId, Long arrivalStationId) { + // TODO PathService 구현 + return new PathResponse(List.of(), 0, 0); + } + +} diff --git a/src/main/java/subway/dto/PathResponse.java b/src/main/java/subway/dto/PathResponse.java new file mode 100644 index 00000000..e9867cb7 --- /dev/null +++ b/src/main/java/subway/dto/PathResponse.java @@ -0,0 +1,27 @@ +package subway.dto; + +import java.util.List; + +public class PathResponse { + private List path; + private int totalDistance; + private int price; + + public PathResponse(List path, int totalDistance, int price) { + this.path = path; + this.totalDistance = totalDistance; + this.price = price; + } + + public List getPath() { + return path; + } + + public int getTotalDistance() { + return totalDistance; + } + + public int getPrice() { + return price; + } +} diff --git a/src/main/java/subway/ui/PathController.java b/src/main/java/subway/ui/PathController.java new file mode 100644 index 00000000..b0372a3b --- /dev/null +++ b/src/main/java/subway/ui/PathController.java @@ -0,0 +1,25 @@ +package subway.ui; + +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 subway.application.PathService; +import subway.dto.PathResponse; + +@RestController +@RequestMapping("/path") +public class PathController { + + private final PathService pathService; + + public PathController(PathService pathService) { + this.pathService = pathService; + } + + @GetMapping + public PathResponse findShortestPath(@RequestParam Long departureStationId, @RequestParam Long arrivalStationId) { + return pathService.findShortestPath(departureStationId, arrivalStationId); + } + +} diff --git a/src/test/java/subway/integration/PathIntegrationTest.java b/src/test/java/subway/integration/PathIntegrationTest.java new file mode 100644 index 00000000..4a740b50 --- /dev/null +++ b/src/test/java/subway/integration/PathIntegrationTest.java @@ -0,0 +1,147 @@ +package subway.integration; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.jdbc.Sql; +import subway.dao.StationDao; +import subway.domain.Station; +import subway.dto.PathResponse; +import subway.dto.StationResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Sql(value = "classpath:/path-testdata.sql") +@DisplayName("경로 조회 기능") +public class PathIntegrationTest extends IntegrationTest { + + @Autowired + private StationDao stationDao; + + @DisplayName("주어진 역 사이의 최단 경로를 조회한다") + @MethodSource("findShortestPathSource") + @ParameterizedTest(name = "{3}") + void findShortestPath(Long departureStationId, Long arrivalStationId, PathResponse expectedPathResponse, String displayName) { + // given, when + ExtractableResponse response = RestAssured + .given().log().all() + .param("departureStationId", departureStationId) + .param("arrivalStationId", arrivalStationId) + .when().get("/path") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + + PathResponse pathResponse = response.as(PathResponse.class); + assertThat(pathResponse) + .usingRecursiveComparison() + .isEqualTo(expectedPathResponse); + } + + private Stream findShortestPathSource() { + return Stream.of( + Arguments.of(1, 3, + new PathResponse(getStationResponses(1, 2, 3), 5, 1_250), + "id=1에서 id=3까지의 최단 경로"), + Arguments.of(1, 5, + new PathResponse(getStationResponses(1, 2, 3, 4, 5), 18, 1_450), + "id=1에서 id=5까지의 최단 경로"), + Arguments.of(1, 7, + new PathResponse(getStationResponses(1, 2, 3, 4, 5, 6, 7), 78, 2_450), + "id=1에서 id=7까지의 최단 경로"), + Arguments.of(1, 10, + new PathResponse(getStationResponses(1, 2, 3, 9, 10), 15, 1_350), + "id=1에서 id=10까지의 최단 경로"), + Arguments.of(1, 11, + new PathResponse(getStationResponses(1, 2, 3, 9, 10, 11), 19, 1_450), + "id=1에서 id=11까지의 최단 경로"), + Arguments.of(10, 14, + new PathResponse(getStationResponses(10, 9, 3, 4, 5, 14), 29, 1_650), + "id=10에서 id=14까지의 최단 경로"), + Arguments.of(14, 10, + new PathResponse(getStationResponses(14, 5, 4, 3, 9, 10), 29, 1_650), + "id=14에서 id=10까지의 최단 경로"), + Arguments.of(4, 16, + new PathResponse(getStationResponses(4, 5, 14, 15, 16), 18, 1_450), + "id=4에서 id=16까지의 최단 경로") + ); + } + + private List getStationResponses(long... ids) { + List list = new ArrayList<>(); + for (Long id : ids) { + Station station = stationDao.findById(id).get(); + list.add(StationResponse.of(station)); + } + return list; + } + + @DisplayName("동일한 역 사이의 최단 경로를 조회한다") + @Test + void findShortestPathWithSameStation() { + // given, when + ExtractableResponse response = RestAssured + .given().log().all() + .param("departureStationId", 1) + .param("arrivalStationId", 1) + .when().get("/path") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + + PathResponse pathResponse = response.as(PathResponse.class); + assertThat(pathResponse.getPath()) + .flatExtracting(StationResponse::getId).containsOnly(1); + assertThat(pathResponse.getTotalDistance()).isEqualTo(0); + assertThat(pathResponse.getPrice()).isEqualTo(0); + } + + @DisplayName("존재하지 않는 역 사이의 최단 경로를 조회한다") + @Test + void findShortestPathWithNonExistenceStation() { + // given, when + ExtractableResponse response = RestAssured + .given().log().all() + .param("departureStationId", 1) + .param("arrivalStationId", 100) + .when().get("/path") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @DisplayName("경로가 존재하지 않는 역 사이의 최단 경로를 조회한다") + @Test + void findShortestPathWithNonExistencePath() { + // given, when + ExtractableResponse response = RestAssured + .given().log().all() + .param("departureStationId", 1) + .param("arrivalStationId", 17) + .when().get("/path") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } +} diff --git a/src/test/resources/path-testdata.sql b/src/test/resources/path-testdata.sql new file mode 100644 index 00000000..a4f639ac --- /dev/null +++ b/src/test/resources/path-testdata.sql @@ -0,0 +1,51 @@ +-- reset tables +delete from section; +delete from station; +delete from line; + +-- line insert +insert into line(id, name, color) values(1, '3호선', 'bg-orange-600'); +insert into line(id, name, color) values(2, '신분당선', 'bg-red-600'); +insert into line(id, name, color) values(3, '수인분당선', 'bg-yellow-600'); +insert into line(id, name, color) values(4, '7호선', 'bg-green-600'); + +-- station insert +insert into station(id, name) values(1, '교대역'); +insert into station(id, name) values(2, '남부터미널역'); +insert into station(id, name) values(3, '양재역'); +insert into station(id, name) values(4, '매봉역'); +insert into station(id, name) values(5, '도곡역'); +insert into station(id, name) values(6, '대치역'); +insert into station(id, name) values(7, '학여울역'); +insert into station(id, name) values(8, '강남역'); +insert into station(id, name) values(9, '양재시민의숲역'); +insert into station(id, name) values(10, '청계산입구역'); +insert into station(id, name) values(11, '판교역'); +insert into station(id, name) values(12, '선릉역'); +insert into station(id, name) values(13, '한티역'); +insert into station(id, name) values(14, '구룡역'); +insert into station(id, name) values(15, '개포동역'); +insert into station(id, name) values(16, '수내역'); +insert into station(id, name) values(17, '총신대입구역'); +insert into station(id, name) values(18, '내방역'); + +-- section insert +insert into section(line_id, up_station_id, down_station_id, distance) values(1, 1, 2, 2); +insert into section(line_id, up_station_id, down_station_id, distance) values(1, 2, 3, 3); +insert into section(line_id, up_station_id, down_station_id, distance) values(1, 3, 4, 6); +insert into section(line_id, up_station_id, down_station_id, distance) values(1, 4, 5, 7); +insert into section(line_id, up_station_id, down_station_id, distance) values(1, 5, 6, 50); +insert into section(line_id, up_station_id, down_station_id, distance) values(1, 6, 7, 10); + +insert into section(line_id, up_station_id, down_station_id, distance) values(2, 8, 3, 5); +insert into section(line_id, up_station_id, down_station_id, distance) values(2, 3, 9, 7); +insert into section(line_id, up_station_id, down_station_id, distance) values(2, 9, 10, 3); +insert into section(line_id, up_station_id, down_station_id, distance) values(2, 10, 11, 4); + +insert into section(line_id, up_station_id, down_station_id, distance) values(3, 12, 13, 1); +insert into section(line_id, up_station_id, down_station_id, distance) values(3, 13, 5, 2); +insert into section(line_id, up_station_id, down_station_id, distance) values(3, 5, 14, 6); +insert into section(line_id, up_station_id, down_station_id, distance) values(3, 14, 15, 2); +insert into section(line_id, up_station_id, down_station_id, distance) values(3, 15, 16, 3); + +insert into section(line_id, up_station_id, down_station_id, distance) values(4, 17, 18, 4); From 5748df3183ebf3628df6eb1af9f9299b2bcd057a Mon Sep 17 00:00:00 2001 From: gmelon Date: Thu, 15 Jun 2023 21:13:43 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EA=B2=BD=EB=A1=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Service,=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +- src/main/java/subway/README.md | 5 +- .../java/subway/application/PathService.java | 16 --- .../subway/application/path/PathPrice.java | 39 ++++++ .../subway/application/path/PathService.java | 98 ++++++++++++++ src/main/java/subway/ui/PathController.java | 2 +- .../java/subway/ui/StationController.java | 2 +- .../path/FindShortestPathParam.java | 53 ++++++++ .../application/path/PathPriceTest.java | 18 +++ .../application/path/PathServiceTest.java | 121 ++++++++++++++++++ .../integration/PathIntegrationTest.java | 87 ++++++------- 11 files changed, 375 insertions(+), 69 deletions(-) delete mode 100644 src/main/java/subway/application/PathService.java create mode 100644 src/main/java/subway/application/path/PathPrice.java create mode 100644 src/main/java/subway/application/path/PathService.java create mode 100644 src/test/java/subway/application/path/FindShortestPathParam.java create mode 100644 src/test/java/subway/application/path/PathPriceTest.java create mode 100644 src/test/java/subway/application/path/PathServiceTest.java diff --git a/build.gradle b/build.gradle index 68d8f255..0c2c3c83 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.jgrapht:jgrapht-core:1.5.2' implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' @@ -24,4 +25,4 @@ dependencies { test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/src/main/java/subway/README.md b/src/main/java/subway/README.md index 94bf97a4..f62f7250 100644 --- a/src/main/java/subway/README.md +++ b/src/main/java/subway/README.md @@ -9,6 +9,9 @@ ## 2단계 기능 요구사항 - [x] 프로덕션, 테스트용 profile 다르게 설정하기 - 프로덕션 DB는 로컬에 저장, 테스트 DB는 인메모리로 동작하도록 설정 -- [ ] 경로 조회 API 구현 +- [x] 경로 조회 API 구현 - 1. 최단 거리 경로, 2. 거리 정보, 3. 해당 거리에 대한 요금 응답 - 다른 노선으로의 환승도 고려해야 함 +- Controller & Test 수정 + - Section, Station Controller에서 PathService 로직 호출하도록 하기 + - PathIntegrationTest 는 기존 데이터 없이 Section/Station Controller 호출만으로 데이터 구성해서 테스트하기 diff --git a/src/main/java/subway/application/PathService.java b/src/main/java/subway/application/PathService.java deleted file mode 100644 index 7a1fd0b5..00000000 --- a/src/main/java/subway/application/PathService.java +++ /dev/null @@ -1,16 +0,0 @@ -package subway.application; - -import org.springframework.stereotype.Service; -import subway.dto.PathResponse; - -import java.util.List; - -@Service -public class PathService { - - public PathResponse findShortestPath(Long departureStationId, Long arrivalStationId) { - // TODO PathService 구현 - return new PathResponse(List.of(), 0, 0); - } - -} diff --git a/src/main/java/subway/application/path/PathPrice.java b/src/main/java/subway/application/path/PathPrice.java new file mode 100644 index 00000000..53e52084 --- /dev/null +++ b/src/main/java/subway/application/path/PathPrice.java @@ -0,0 +1,39 @@ +package subway.application.path; + +import java.util.Arrays; + +public enum PathPrice { + + DEFAULT(0, 10, 1_250, 0), + FIRST_STEP(10, 50, 1_250, 5), + SECOND_STEP(50, Integer.MAX_VALUE, 2_050, 8); + + private final int distanceUnderLimit; + private final int distanceUpperLimit; + private final int defaultPrice; + private final int billingUnit; + + PathPrice(int distanceUnderLimit, int distanceUpperLimit, int defaultPrice, int billingUnit) { + this.distanceUnderLimit = distanceUnderLimit; + this.distanceUpperLimit = distanceUpperLimit; + this.defaultPrice = defaultPrice; + this.billingUnit = billingUnit; + } + + public static int calculate(int totalDistance) { + PathPrice pathPrice = Arrays.stream(values()) + .filter(value -> totalDistance <= value.distanceUpperLimit) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("경로의 전체 거리 값이 잘못되었습니다.")); + + return pathPrice.defaultPrice + + calculateOverPrice(totalDistance - pathPrice.distanceUnderLimit,pathPrice.billingUnit); + } + + private static int calculateOverPrice(int distance, int billingUnit) { + if (billingUnit == 0) { + return 0; + } + return 100 * ((distance - 1) / billingUnit + 1); + } +} diff --git a/src/main/java/subway/application/path/PathService.java b/src/main/java/subway/application/path/PathService.java new file mode 100644 index 00000000..31658318 --- /dev/null +++ b/src/main/java/subway/application/path/PathService.java @@ -0,0 +1,98 @@ +package subway.application.path; + +import org.jgrapht.GraphPath; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedPseudograph; +import org.springframework.stereotype.Service; +import subway.dao.SectionDao; +import subway.dao.StationDao; +import subway.domain.Section; +import subway.domain.Station; +import subway.dto.PathResponse; +import subway.dto.StationResponse; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class PathService { + + static final WeightedPseudograph GRAPH = new WeightedPseudograph<>(DefaultWeightedEdge.class); + + private final StationDao stationDao; + private final SectionDao sectionDao; + + public PathService(StationDao stationDao, SectionDao sectionDao) { + this.stationDao = stationDao; + this.sectionDao = sectionDao; + } + + @PostConstruct + void addExistingDataToGraph() { + List stations = stationDao.findAll(); + for (Station station : stations) { + addVertex(station.getId()); + } + + List
sections = sectionDao.findAll(); + for (Section section : sections) { + addEdge(section.getUpStation().getId(), section.getDownStation().getId(), section.getDistance()); + } + } + + public PathResponse findShortestPath(Long departureStationId, Long arrivalStationId) { + validateStationsAreNotSame(departureStationId, arrivalStationId); + + GraphPath path = getPath(departureStationId, arrivalStationId); + List stationResponses = path.getVertexList().stream() + .map(stationId -> stationDao.findById(stationId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Station 입니다."))) + .map(StationResponse::of) + .collect(Collectors.toList()); + int totalDistance = (int) path.getWeight(); + + return new PathResponse(stationResponses, totalDistance, PathPrice.calculate(totalDistance)); + } + + private void validateStationsAreNotSame(Long departureStationId, Long arrivalStationId) { + if (departureStationId.equals(arrivalStationId)) { + throw new IllegalArgumentException("출발역과 도착역이 동일합니다."); + } + } + + private GraphPath getPath(Long departureStationId, Long arrivalStationId) { + DijkstraShortestPath dijkstra = new DijkstraShortestPath<>(GRAPH); + + GraphPath path; + try { + path = dijkstra.getPath(departureStationId, arrivalStationId); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("출발역 또는 도착역이 존재하지 않습니다."); + } + + if (path == null) { + throw new IllegalArgumentException("출발역과 도착역 사이에 경로가 존재하지 않습니다."); + } + + return path; + } + + public void addVertex(Long vertex) { + GRAPH.addVertex(vertex); + } + + public void removeVertex(Long vertex) { + GRAPH.removeVertex(vertex); + } + + public void addEdge(Long startVertex, Long endVertex, int weight) { + GRAPH.setEdgeWeight(GRAPH.addEdge(startVertex, endVertex), weight); + } + + public void removeEdge(Long startVertex, Long endVertex) { + GRAPH.removeEdge(startVertex, endVertex); + } + +} diff --git a/src/main/java/subway/ui/PathController.java b/src/main/java/subway/ui/PathController.java index b0372a3b..f05f1ec6 100644 --- a/src/main/java/subway/ui/PathController.java +++ b/src/main/java/subway/ui/PathController.java @@ -4,7 +4,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import subway.application.PathService; +import subway.application.path.PathService; import subway.dto.PathResponse; @RestController diff --git a/src/main/java/subway/ui/StationController.java b/src/main/java/subway/ui/StationController.java index 5bf52a9a..64082398 100644 --- a/src/main/java/subway/ui/StationController.java +++ b/src/main/java/subway/ui/StationController.java @@ -2,9 +2,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import subway.application.StationService; import subway.dto.StationRequest; import subway.dto.StationResponse; -import subway.application.StationService; import java.net.URI; import java.sql.SQLException; diff --git a/src/test/java/subway/application/path/FindShortestPathParam.java b/src/test/java/subway/application/path/FindShortestPathParam.java new file mode 100644 index 00000000..5c6581f5 --- /dev/null +++ b/src/test/java/subway/application/path/FindShortestPathParam.java @@ -0,0 +1,53 @@ +package subway.application.path; + +import org.junit.jupiter.params.provider.Arguments; +import subway.domain.Station; +import subway.dto.PathResponse; +import subway.dto.StationResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class FindShortestPathParam { + + public static Stream findShortestPathSource() { + return Stream.of( + Arguments.of(1, 3, + new PathResponse(getStationResponses(1, 2, 3), 5, 1_250), + "id=1에서 id=3까지의 최단 경로"), + Arguments.of(1, 5, + new PathResponse(getStationResponses(1, 2, 3, 4, 5), 18, 1_450), + "id=1에서 id=5까지의 최단 경로"), + Arguments.of(1, 7, + new PathResponse(getStationResponses(1, 2, 3, 4, 5, 6, 7), 78, 2_450), + "id=1에서 id=7까지의 최단 경로"), + Arguments.of(1, 10, + new PathResponse(getStationResponses(1, 2, 3, 9, 10), 15, 1_350), + "id=1에서 id=10까지의 최단 경로"), + Arguments.of(1, 11, + new PathResponse(getStationResponses(1, 2, 3, 9, 10, 11), 19, 1_450), + "id=1에서 id=11까지의 최단 경로"), + Arguments.of(10, 14, + new PathResponse(getStationResponses(10, 9, 3, 4, 5, 14), 29, 1_650), + "id=10에서 id=14까지의 최단 경로"), + Arguments.of(14, 10, + new PathResponse(getStationResponses(14, 5, 4, 3, 9, 10), 29, 1_650), + "id=14에서 id=10까지의 최단 경로"), + Arguments.of(4, 16, + new PathResponse(getStationResponses(4, 5, 14, 15, 16), 18, 1_450), + "id=4에서 id=16까지의 최단 경로") + ); + } + + private static List getStationResponses(long... ids) { + List list = new ArrayList<>(); + for (Long id : ids) { + Station station = new Station(id, null); + list.add(StationResponse.of(station)); + } + return list; + } + + +} diff --git a/src/test/java/subway/application/path/PathPriceTest.java b/src/test/java/subway/application/path/PathPriceTest.java new file mode 100644 index 00000000..130a63c9 --- /dev/null +++ b/src/test/java/subway/application/path/PathPriceTest.java @@ -0,0 +1,18 @@ +package subway.application.path; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("경로 요금 계산 기능") +class PathPriceTest { + + @DisplayName("주어진 경로의 요금을 계산한다") + @ParameterizedTest + @CsvSource(value = {"5:1_250", "10:1_250", "11:1_350", "15:1_350", "18:1_450", "50:2_050", "51:2_150", "58:2_150", "59:2_250", "78:2_450"}, delimiter = ':') + public void calculatePrice(int totalDistance, int expectedPrice) { + assertThat(PathPrice.calculate(totalDistance)).isEqualTo(expectedPrice); + } +} diff --git a/src/test/java/subway/application/path/PathServiceTest.java b/src/test/java/subway/application/path/PathServiceTest.java new file mode 100644 index 00000000..da32b07a --- /dev/null +++ b/src/test/java/subway/application/path/PathServiceTest.java @@ -0,0 +1,121 @@ +package subway.application.path; + +import org.jgrapht.graph.DefaultWeightedEdge; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.jdbc.Sql; +import subway.dto.PathResponse; +import subway.dto.StationResponse; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("경로 조회 관련 기능") +@Sql(value = "classpath:/path-testdata.sql") +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +class PathServiceTest { + + @Autowired + private PathService pathService; + + @DisplayName("두 역 사이의 최단 경로를 찾는다") + @MethodSource("subway.application.path.FindShortestPathParam#findShortestPathSource") + @ParameterizedTest(name = "{3}") + void findShortestPath(long departureStationId, long arrivalStationId, PathResponse expectedPathResponse, String displayName) { + // given + pathService.addExistingDataToGraph(); + + // when + PathResponse pathResponse = pathService.findShortestPath(departureStationId, arrivalStationId); + + // then + assertThat(pathResponse) + .usingRecursiveComparison() + .ignoringFields("path") + .isEqualTo(expectedPathResponse); + + List stationIds = pathResponse.getPath().stream() + .map(StationResponse::getId) + .collect(Collectors.toList()); + assertThat(pathResponse.getPath()) + .flatExtracting(StationResponse::getId).isEqualTo(stationIds); + } + + @DisplayName("동일한 역 사이의 최단 경로를 조회한다") + @Test + void findShortestPathWithSameStation() { + assertThatThrownBy(() -> pathService.findShortestPath(1L, 1L)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("존재하지 않는 역 사이의 최단 경로를 조회한다") + @Test + void findShortestPathWithNonExistenceStation() { + assertThatThrownBy(() -> pathService.findShortestPath(1L, 100L)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("경로가 존재하지 않는 역 사이의 최단 경로를 조회한다") + @Test + void findShortestPathWithNonExistencePath() { + assertThatThrownBy(() -> pathService.findShortestPath(1L, 17L)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("그래프에 정점(Station)을 추가한다") + @Test + void addVertex() { + // when + pathService.addVertex(100L); + + // then + assertThat(PathService.GRAPH.containsVertex(100L)).isTrue(); + } + + @DisplayName("그래프에서 정점(Station)을 제거한다") + @Test + void removeVertex() { + // when + pathService.removeVertex(1L); + + // then + assertThat(PathService.GRAPH.containsVertex(1L)).isFalse(); + } + + @DisplayName("그래프에 간선(Section)을 추가한다") + @Test + void addEdge() { + // given + pathService.addVertex(100L); + pathService.addVertex(101L); + + // when + pathService.addEdge(100L, 101L, 10); + + // then + DefaultWeightedEdge edge = PathService.GRAPH.getEdge(100L, 101L); + + assertThat(edge).isNotNull(); + assertThat(PathService.GRAPH.getEdgeWeight(edge)).isEqualTo(10); + } + + @DisplayName("그래프에서 간선(Section)을 제거한다") + @Test + void removeEdge() { + // when + pathService.removeEdge(1L, 2L); + + // then + assertThat(PathService.GRAPH.getEdge(1L, 2L)).isNull(); + } + +} diff --git a/src/test/java/subway/integration/PathIntegrationTest.java b/src/test/java/subway/integration/PathIntegrationTest.java index 4a740b50..e4eda8c9 100644 --- a/src/test/java/subway/integration/PathIntegrationTest.java +++ b/src/test/java/subway/integration/PathIntegrationTest.java @@ -3,23 +3,25 @@ import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.test.context.jdbc.Sql; +import subway.application.path.PathService; +import subway.dao.SectionDao; import subway.dao.StationDao; +import subway.domain.Section; import subway.domain.Station; import subway.dto.PathResponse; import subway.dto.StationResponse; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -30,11 +32,35 @@ public class PathIntegrationTest extends IntegrationTest { @Autowired private StationDao stationDao; + @Autowired + private SectionDao sectionDao; + @Autowired + private PathService pathService; + + @Override + @BeforeEach + public void setUp() { + super.setUp(); + + addExistingDataToGraph(); + } + + private void addExistingDataToGraph() { + List stations = stationDao.findAll(); + for (Station station : stations) { + pathService.addVertex(station.getId()); + } + + List
sections = sectionDao.findAll(); + for (Section section : sections) { + pathService.addEdge(section.getUpStation().getId(), section.getDownStation().getId(), section.getDistance()); + } + } @DisplayName("주어진 역 사이의 최단 경로를 조회한다") - @MethodSource("findShortestPathSource") + @MethodSource("subway.application.path.FindShortestPathParam#findShortestPathSource") @ParameterizedTest(name = "{3}") - void findShortestPath(Long departureStationId, Long arrivalStationId, PathResponse expectedPathResponse, String displayName) { + void findShortestPath(long departureStationId, long arrivalStationId, PathResponse expectedPathResponse, String displayName) { // given, when ExtractableResponse response = RestAssured .given().log().all() @@ -50,45 +76,14 @@ void findShortestPath(Long departureStationId, Long arrivalStationId, PathRespon PathResponse pathResponse = response.as(PathResponse.class); assertThat(pathResponse) .usingRecursiveComparison() + .ignoringFields("path") .isEqualTo(expectedPathResponse); - } - - private Stream findShortestPathSource() { - return Stream.of( - Arguments.of(1, 3, - new PathResponse(getStationResponses(1, 2, 3), 5, 1_250), - "id=1에서 id=3까지의 최단 경로"), - Arguments.of(1, 5, - new PathResponse(getStationResponses(1, 2, 3, 4, 5), 18, 1_450), - "id=1에서 id=5까지의 최단 경로"), - Arguments.of(1, 7, - new PathResponse(getStationResponses(1, 2, 3, 4, 5, 6, 7), 78, 2_450), - "id=1에서 id=7까지의 최단 경로"), - Arguments.of(1, 10, - new PathResponse(getStationResponses(1, 2, 3, 9, 10), 15, 1_350), - "id=1에서 id=10까지의 최단 경로"), - Arguments.of(1, 11, - new PathResponse(getStationResponses(1, 2, 3, 9, 10, 11), 19, 1_450), - "id=1에서 id=11까지의 최단 경로"), - Arguments.of(10, 14, - new PathResponse(getStationResponses(10, 9, 3, 4, 5, 14), 29, 1_650), - "id=10에서 id=14까지의 최단 경로"), - Arguments.of(14, 10, - new PathResponse(getStationResponses(14, 5, 4, 3, 9, 10), 29, 1_650), - "id=14에서 id=10까지의 최단 경로"), - Arguments.of(4, 16, - new PathResponse(getStationResponses(4, 5, 14, 15, 16), 18, 1_450), - "id=4에서 id=16까지의 최단 경로") - ); - } - private List getStationResponses(long... ids) { - List list = new ArrayList<>(); - for (Long id : ids) { - Station station = stationDao.findById(id).get(); - list.add(StationResponse.of(station)); - } - return list; + List stationIds = pathResponse.getPath().stream() + .map(StationResponse::getId) + .collect(Collectors.toList()); + assertThat(pathResponse.getPath()) + .flatExtracting(StationResponse::getId).isEqualTo(stationIds); } @DisplayName("동일한 역 사이의 최단 경로를 조회한다") @@ -104,13 +99,7 @@ void findShortestPathWithSameStation() { .extract(); // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - - PathResponse pathResponse = response.as(PathResponse.class); - assertThat(pathResponse.getPath()) - .flatExtracting(StationResponse::getId).containsOnly(1); - assertThat(pathResponse.getTotalDistance()).isEqualTo(0); - assertThat(pathResponse.getPrice()).isEqualTo(0); + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @DisplayName("존재하지 않는 역 사이의 최단 경로를 조회한다") From 7c1e9dbd545ed1396fb4e8751dce9f79fad67918 Mon Sep 17 00:00:00 2001 From: gmelon Date: Thu, 15 Jun 2023 21:59:12 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20Section=EA=B3=BC=20Station=20Contro?= =?UTF-8?q?ller=EC=97=90=EC=84=9C=20pathService=EB=A5=BC=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95,=20Pa?= =?UTF-8?q?th=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/application/SectionService.java | 10 ++ .../java/subway/ui/SectionController.java | 11 +- .../java/subway/ui/StationController.java | 9 +- .../integration/PathIntegrationTest.java | 101 +++++++++++++----- 4 files changed, 102 insertions(+), 29 deletions(-) diff --git a/src/main/java/subway/application/SectionService.java b/src/main/java/subway/application/SectionService.java index 9926157e..77ba7f70 100644 --- a/src/main/java/subway/application/SectionService.java +++ b/src/main/java/subway/application/SectionService.java @@ -91,4 +91,14 @@ private boolean isNotLastSection(List
sections, Long sectionId) { private Section getLastSection(List
sections) { return sections.get(sections.size() - 1); } + + public SectionResponse findSectionResponseById(Long id) { + Section persistSection = findSectionById(id); + return SectionResponse.of(persistSection); + } + + private Section findSectionById(Long id) { + return sectionDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Section 입니다. id = " + id)); + } } diff --git a/src/main/java/subway/ui/SectionController.java b/src/main/java/subway/ui/SectionController.java index 451ab0d8..3053f50b 100644 --- a/src/main/java/subway/ui/SectionController.java +++ b/src/main/java/subway/ui/SectionController.java @@ -3,6 +3,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import subway.application.SectionService; +import subway.application.path.PathService; import subway.dto.SectionRequest; import subway.dto.SectionResponse; @@ -13,20 +14,28 @@ public class SectionController { private final SectionService sectionService; + private final PathService pathService; - public SectionController(SectionService sectionService) { + public SectionController(SectionService sectionService, PathService pathService) { this.sectionService = sectionService; + this.pathService = pathService; } @PostMapping public ResponseEntity createSection(@PathVariable Long lineId, @RequestBody SectionRequest request) { SectionResponse response = sectionService.saveSection(lineId, request); + pathService.addEdge(request.getUpStationId(), request.getDownStationId(), request.getDistance()); + return ResponseEntity.created(URI.create("/lines/" + lineId + "/sections/" + response.getId())).body(response); } @DeleteMapping("/{sectionId}") public ResponseEntity deleteSection(@PathVariable Long lineId, @PathVariable Long sectionId) { + SectionResponse sectionResponse = sectionService.findSectionResponseById(sectionId); + sectionService.deleteSectionById(lineId, sectionId); + pathService.removeEdge(sectionResponse.getUpStationId(), sectionResponse.getDownStationId()); + return ResponseEntity.noContent().build(); } diff --git a/src/main/java/subway/ui/StationController.java b/src/main/java/subway/ui/StationController.java index 64082398..34027f72 100644 --- a/src/main/java/subway/ui/StationController.java +++ b/src/main/java/subway/ui/StationController.java @@ -3,6 +3,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import subway.application.StationService; +import subway.application.path.PathService; import subway.dto.StationRequest; import subway.dto.StationResponse; @@ -14,14 +15,18 @@ @RequestMapping("/stations") public class StationController { private final StationService stationService; + private final PathService pathService; - public StationController(StationService stationService) { + public StationController(StationService stationService, PathService pathService) { this.stationService = stationService; + this.pathService = pathService; } @PostMapping public ResponseEntity createStation(@RequestBody StationRequest stationRequest) { StationResponse station = stationService.saveStation(stationRequest); + pathService.addVertex(station.getId()); + return ResponseEntity.created(URI.create("/stations/" + station.getId())).body(station); } @@ -44,6 +49,8 @@ public ResponseEntity updateStation(@PathVariable Long id, @RequestBody St @DeleteMapping("/{id}") public ResponseEntity deleteStation(@PathVariable Long id) { stationService.deleteStationById(id); + pathService.removeVertex(id); + return ResponseEntity.noContent().build(); } diff --git a/src/test/java/subway/integration/PathIntegrationTest.java b/src/test/java/subway/integration/PathIntegrationTest.java index e4eda8c9..6854df2c 100644 --- a/src/test/java/subway/integration/PathIntegrationTest.java +++ b/src/test/java/subway/integration/PathIntegrationTest.java @@ -9,16 +9,9 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.test.context.jdbc.Sql; -import subway.application.path.PathService; -import subway.dao.SectionDao; -import subway.dao.StationDao; -import subway.domain.Section; -import subway.domain.Station; -import subway.dto.PathResponse; -import subway.dto.StationResponse; +import org.springframework.http.MediaType; +import subway.dto.*; import java.util.List; import java.util.stream.Collectors; @@ -26,34 +19,88 @@ import static org.assertj.core.api.Assertions.assertThat; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@Sql(value = "classpath:/path-testdata.sql") @DisplayName("경로 조회 기능") public class PathIntegrationTest extends IntegrationTest { - @Autowired - private StationDao stationDao; - @Autowired - private SectionDao sectionDao; - @Autowired - private PathService pathService; - @Override @BeforeEach public void setUp() { super.setUp(); - - addExistingDataToGraph(); + addTestData(); } - private void addExistingDataToGraph() { - List stations = stationDao.findAll(); - for (Station station : stations) { - pathService.addVertex(station.getId()); - } + private void addTestData() { + List lineRequests = List.of( + new LineRequest("3호선", "bg-orange-600"), + new LineRequest("신분당선", "bg-red-600"), + new LineRequest("수인분당선", "bg-yellow-600"), + new LineRequest("7호선", "bg-green-600") + ); + sendPostRequests("/lines", lineRequests); + + List stationRequests = List.of( + new StationRequest("교대역"), + new StationRequest("남부터미널역"), + new StationRequest("양재역"), + new StationRequest("매봉역"), + new StationRequest("도곡역"), + new StationRequest("대치역"), + new StationRequest("학여울역"), + new StationRequest("강남역"), + new StationRequest("양재시민의숲역"), + new StationRequest("청계산입구역"), + new StationRequest("판교역"), + new StationRequest("선릉역"), + new StationRequest("한티역"), + new StationRequest("구룡역"), + new StationRequest("개포동역"), + new StationRequest("수내역"), + new StationRequest("총신대입구역"), + new StationRequest("내방역") + ); + sendPostRequests("/stations", stationRequests); + + List line1SectionRequests = List.of( + new SectionRequest(1L, 2L, 2), + new SectionRequest(2L, 3L, 3), + new SectionRequest(3L, 4L, 6), + new SectionRequest(4L, 5L, 7), + new SectionRequest(5L, 6L, 50), + new SectionRequest(6L, 7L, 10) + ); + sendPostRequests("/lines/1/sections", line1SectionRequests); + + List line2SectionRequests = List.of( + new SectionRequest(8L, 3L, 5), + new SectionRequest(3L, 9L, 7), + new SectionRequest(9L, 10L, 3), + new SectionRequest(10L, 11L, 4) + ); + sendPostRequests("/lines/2/sections", line2SectionRequests); + + List line3SectionRequests = List.of( + new SectionRequest(12L, 13L, 1), + new SectionRequest(13L, 5L, 2), + new SectionRequest(5L, 14L, 6), + new SectionRequest(14L, 15L, 2), + new SectionRequest(15L, 16L, 3) + ); + sendPostRequests("/lines/3/sections", line3SectionRequests); + + List line4SectionRequests = List.of( + new SectionRequest(17L, 18L, 4) + ); + sendPostRequests("/lines/4/sections", line4SectionRequests); + } - List
sections = sectionDao.findAll(); - for (Section section : sections) { - pathService.addEdge(section.getUpStation().getId(), section.getDownStation().getId(), section.getDistance()); + private void sendPostRequests(String url, List requestBodies) { + for (Object requestBody : requestBodies) { + RestAssured + .given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(requestBody) + .when().post(url) + .then(); } }