From a4327ac5ec3a726fe4443e3852a7a910dbd4ce22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9A=B0=EC=B0=BD?= Date: Sun, 14 May 2023 10:31:10 +0900 Subject: [PATCH 01/40] =?UTF-8?q?[1=EB=8B=A8=EA=B3=84=20-=20=EC=A7=80?= =?UTF-8?q?=ED=95=98=EC=B2=A0=20=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5]=20=EB=8B=A4=EC=A6=90(=EC=B5=9C=EC=9A=B0?= =?UTF-8?q?=EC=B0=BD)=20=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20(#14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 기능 요구사항 추가 Co-authored-by: woo-chang * feat: 역 이름 길이 검증 기능 추가 Co-authored-by: woo-chang * feat: 역 이름 형식 검증 기능 추가 Co-authored-by: woo-chang * feat: 역 이름이 공백일 때 검증 기능 추가 Co-authored-by: woo-chang * test: 이름에 대한 생성 테스트 추가 Co-authored-by: woo-chang * feat: 거리 검증 기능 추가 Co-authored-by: woo-chang * feat: 두 역과 역 사이의 거리를 가지는 구역 추가 Co-authored-by: woo-chang * remove: 사용하지 않는 파일 정리 Co-authored-by: woo-chang * feat: 노선 이름 검증 기능 추가 Co-authored-by: woo-chang * feat: 노선 색 검증 기능 추가 Co-authored-by: woo-chang * docs: API 명세 작성 Co-authored-by: woo-chang * feat: 역 등록 API 추가 Co-authored-by: woo-chang * feat: 역 정보 조회 API 추가 Co-authored-by: woo-chang * docs: API 명세 수정 Co-authored-by: woo-chang * feat: 노선 등록 API 추가 Co-authored-by: woo-chang * feat: 노선 조회 API 추가 Co-authored-by: woo-chang * feat: 노선 목록 조회 API 추가 Co-authored-by: woo-chang * feat: 노선 역 등록 API 추가 Co-authored-by: woo-chang * feat: 노선에서 역 삭제 API 추가 Co-authored-by: woo-chang * feat: 노선에 새로운 역 등록 기능 추가 Co-authored-by: woo-chang * feat: 노선에 존재하는 역 제거 기능 추가 Co-authored-by: woo-chang * test: 노선에 역 추가, 삭제 후 거리를 검증하는 테스트 추가 Co-authored-by: woo-chang * feat: 역을 생성하는 로직 추가 Co-authored-by: woo-chang * feat: 역을 조회하는 로직 추가 Co-authored-by: woo-chang * feat: 노선을 등록하는 로직 추가 Co-authored-by: woo-chang * refactor: 노선 목록 조회시 노선 안의 역 정보를 포함하도록 변경 Co-authored-by: woo-chang * feat: 노선 조회 로직 추가 Co-authored-by: woo-chang * feat: 노선 목록 조회 로직 추가 Co-authored-by: woo-chang * feat: 노선에 역 등록 로직 추가 Co-authored-by: woo-chang * feat: 노션 역 삭제 로직 추가 Co-authored-by: woo-chang * feat: DB에 역을 저장하는 기능 추가 Co-authored-by: woo-chang * feat: DB에 노선을 저장하는 기능 추가 Co-authored-by: woo-chang * feat: DB에 구간을 저장하는 기능 추가 Co-authored-by: woo-chang * feat: DB에서 모든 노선 조회 기능 추가 Co-authored-by: woo-chang * feat: DB에서 노선 업데이트 기능 추가 Co-authored-by: woo-chang * feat: DB에서 특정 노선의 구간을 삭제하는 기능 추가 Co-authored-by: woo-chang * feat: DB에 모든 구간 정보를 저장하는 기능 추가 Co-authored-by: woo-chang * feat: 역 ID로 역 정보 조회 기능 추가 Co-authored-by: woo-chang * feat: 노선 저장 기능 추가 Co-authored-by: woo-chang * feat: 노선 ID로 정보 조회 기능 추가 Co-authored-by: woo-chang * feat: 노선 정보 조회 기능 추가 Co-authored-by: woo-chang * feat: 노선 정보 업데이트 기능 추가 Co-authored-by: woo-chang * refactor: 모든 노선 정보 조회 시 노선을 다시 조회하지 않도록 수정 Co-authored-by: woo-chang * feat: 예외 발생 시 로그를 남기는 기능 추가 Co-authored-by: woo-chang * refactor: 유효한 구간 정보가 순서대로 입력되지 않아도 정상적으로 노선을 생성하도록 변경 Co-authored-by: woo-chang * refactor: 컨벤션 통일 Co-authored-by: woo-chang * refactor: Service에 Transactional 적용 Co-authored-by: woo-chang * test: Controller 통합테스트 추가 Co-authored-by: woo-chang * feat: 내부 예외처리 기능 추가 Co-authored-by: woo-chang * docs: README 업데이트 Co-authored-by: woo-chang * fix: 역간 거리의 최대길이 제한 삭제 Co-authored-by: woo-chang * refactor: 예외 메시지 수정 Co-authored-by: woo-chang * test: 거리에 대한 테스트 수정 Co-authored-by: woo-chang * docs: 수정된 기능 반영 * chore: 코드 컨벤션 수정 * feat: Swagger를 이용해 API 문서화 * refactor: 예상할 수 없는 예외에 대한 로그 레벨 수정 * refactor: 노선 색에 대한 예외 세분화 * test: 의도와 다른 테스트명 수정 * refactor: 예외에서 상태 코드 관리 책임 분리 * refactor: request 기본 생성자 접근 제어자 수정 * refactor: 도메인 검증 통일성 있게 수정 * refactor: service에서 repository를 사용하도록 수정 * refactor: 도메인 정적 팩토리 메서드 수정 * refactor: 코드 컨벤션 수정 --------- Co-authored-by: bjk1649 --- README.md | 46 ++- build.gradle | 5 +- src/main/java/subway/SubwayApplication.java | 6 +- .../java/subway/application/LineService.java | 53 ---- .../subway/application/StationService.java | 44 --- .../subway/config/SwaggerConfiguration.java | 22 ++ .../subway/controller/ControllerAdvice.java | 58 ++++ .../subway/controller/LineController.java | 73 +++++ .../subway/controller/StationController.java | 42 +++ .../dto/request/LineCreateRequest.java | 31 ++ .../dto/request/SectionCreateRequest.java | 42 +++ .../dto/request/StationCreateRequest.java | 22 ++ .../controller/dto/response/LineResponse.java | 55 ++++ .../dto/response/LinesResponse.java | 18 ++ .../dto/response/StationResponse.java | 30 ++ src/main/java/subway/dao/LineDao.java | 76 ++--- src/main/java/subway/dao/SectionDao.java | 81 ++++++ src/main/java/subway/dao/StationDao.java | 65 ++--- src/main/java/subway/domain/Line.java | 48 --- src/main/java/subway/domain/Station.java | 41 --- src/main/java/subway/domain/line/Color.java | 34 +++ src/main/java/subway/domain/line/Line.java | 178 ++++++++++++ src/main/java/subway/domain/line/Name.java | 30 ++ .../java/subway/domain/section/Distance.java | 25 ++ .../java/subway/domain/section/Section.java | 35 +++ .../java/subway/domain/section/Sections.java | 63 ++++ src/main/java/subway/domain/station/Name.java | 34 +++ .../java/subway/domain/station/Station.java | 45 +++ src/main/java/subway/dto/LineRequest.java | 23 -- src/main/java/subway/dto/LineResponse.java | 31 -- src/main/java/subway/dto/StationRequest.java | 16 - src/main/java/subway/dto/StationResponse.java | 25 -- src/main/java/subway/entity/LineEntity.java | 54 ++++ .../java/subway/entity/SectionEntity.java | 91 ++++++ .../java/subway/entity/StationEntity.java | 48 +++ .../exception/InvalidColorException.java | 8 + .../exception/InvalidDistanceException.java | 8 + .../exception/InvalidLineException.java | 8 + .../exception/InvalidLineNameException.java | 8 + .../exception/InvalidSectionException.java | 8 + .../exception/InvalidStationException.java | 8 + .../InvalidStationNameException.java | 8 + .../subway/exception/SubwayException.java | 8 + .../subway/repository/LineRepository.java | 83 ++++++ .../subway/repository/StationRepository.java | 28 ++ src/main/java/subway/service/LineService.java | 66 +++++ .../java/subway/service/StationService.java | 30 ++ src/main/java/subway/ui/LineController.java | 55 ---- .../java/subway/ui/StationController.java | 54 ---- src/main/resources/application.yml | 28 ++ src/main/resources/data.sql | 27 ++ src/main/resources/dummy.sql | 5 + src/main/resources/schema.sql | 14 - .../subway/Integration/IntegrationTest.java | 20 ++ .../LineControllerIntegrationTest.java | 261 +++++++++++++++++ .../StationControllerIntegrationTest.java | 88 ++++++ .../java/subway/SubwayApplicationTests.java | 6 +- .../subway/controller/LineControllerTest.java | 275 ++++++++++++++++++ .../controller/StationControllerTest.java | 99 +++++++ src/test/java/subway/dao/LineDaoTest.java | 103 +++++++ src/test/java/subway/dao/SectionDaoTest.java | 94 ++++++ src/test/java/subway/dao/StationDaoTest.java | 54 ++++ .../java/subway/domain/line/ColorTest.java | 41 +++ .../java/subway/domain/line/LineTest.java | 192 ++++++++++++ .../java/subway/domain/line/NameTest.java | 38 +++ .../subway/domain/section/DistanceTest.java | 30 ++ .../java/subway/domain/station/NameTest.java | 56 ++++ .../subway/integration/IntegrationTest.java | 19 -- .../integration/LineIntegrationTest.java | 190 ------------ .../integration/StationIntegrationTest.java | 197 ------------- .../subway/repository/LineRepositoryTest.java | 166 +++++++++++ .../repository/StationRepositoryTest.java | 70 +++++ .../java/subway/service/LineServiceTest.java | 216 ++++++++++++++ .../subway/service/StationServiceTest.java | 89 ++++++ src/test/resources/application.yml | 8 + src/test/resources/data.sql | 27 ++ src/test/resources/logback-access.xml | 8 + 77 files changed, 3498 insertions(+), 893 deletions(-) delete mode 100644 src/main/java/subway/application/LineService.java delete mode 100644 src/main/java/subway/application/StationService.java create mode 100644 src/main/java/subway/config/SwaggerConfiguration.java create mode 100644 src/main/java/subway/controller/ControllerAdvice.java create mode 100644 src/main/java/subway/controller/LineController.java create mode 100644 src/main/java/subway/controller/StationController.java create mode 100644 src/main/java/subway/controller/dto/request/LineCreateRequest.java create mode 100644 src/main/java/subway/controller/dto/request/SectionCreateRequest.java create mode 100644 src/main/java/subway/controller/dto/request/StationCreateRequest.java create mode 100644 src/main/java/subway/controller/dto/response/LineResponse.java create mode 100644 src/main/java/subway/controller/dto/response/LinesResponse.java create mode 100644 src/main/java/subway/controller/dto/response/StationResponse.java create mode 100644 src/main/java/subway/dao/SectionDao.java delete mode 100644 src/main/java/subway/domain/Line.java delete mode 100644 src/main/java/subway/domain/Station.java create mode 100644 src/main/java/subway/domain/line/Color.java create mode 100644 src/main/java/subway/domain/line/Line.java create mode 100644 src/main/java/subway/domain/line/Name.java create mode 100644 src/main/java/subway/domain/section/Distance.java create mode 100644 src/main/java/subway/domain/section/Section.java create mode 100644 src/main/java/subway/domain/section/Sections.java create mode 100644 src/main/java/subway/domain/station/Name.java create mode 100644 src/main/java/subway/domain/station/Station.java delete mode 100644 src/main/java/subway/dto/LineRequest.java delete mode 100644 src/main/java/subway/dto/LineResponse.java delete mode 100644 src/main/java/subway/dto/StationRequest.java delete mode 100644 src/main/java/subway/dto/StationResponse.java create mode 100644 src/main/java/subway/entity/LineEntity.java create mode 100644 src/main/java/subway/entity/SectionEntity.java create mode 100644 src/main/java/subway/entity/StationEntity.java create mode 100644 src/main/java/subway/exception/InvalidColorException.java create mode 100644 src/main/java/subway/exception/InvalidDistanceException.java create mode 100644 src/main/java/subway/exception/InvalidLineException.java create mode 100644 src/main/java/subway/exception/InvalidLineNameException.java create mode 100644 src/main/java/subway/exception/InvalidSectionException.java create mode 100644 src/main/java/subway/exception/InvalidStationException.java create mode 100644 src/main/java/subway/exception/InvalidStationNameException.java create mode 100644 src/main/java/subway/exception/SubwayException.java create mode 100644 src/main/java/subway/repository/LineRepository.java create mode 100644 src/main/java/subway/repository/StationRepository.java create mode 100644 src/main/java/subway/service/LineService.java create mode 100644 src/main/java/subway/service/StationService.java delete mode 100644 src/main/java/subway/ui/LineController.java delete mode 100644 src/main/java/subway/ui/StationController.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/dummy.sql delete mode 100644 src/main/resources/schema.sql create mode 100644 src/test/java/subway/Integration/IntegrationTest.java create mode 100644 src/test/java/subway/Integration/LineControllerIntegrationTest.java create mode 100644 src/test/java/subway/Integration/StationControllerIntegrationTest.java create mode 100644 src/test/java/subway/controller/LineControllerTest.java create mode 100644 src/test/java/subway/controller/StationControllerTest.java create mode 100644 src/test/java/subway/dao/LineDaoTest.java create mode 100644 src/test/java/subway/dao/SectionDaoTest.java create mode 100644 src/test/java/subway/dao/StationDaoTest.java create mode 100644 src/test/java/subway/domain/line/ColorTest.java create mode 100644 src/test/java/subway/domain/line/LineTest.java create mode 100644 src/test/java/subway/domain/line/NameTest.java create mode 100644 src/test/java/subway/domain/section/DistanceTest.java create mode 100644 src/test/java/subway/domain/station/NameTest.java delete mode 100644 src/test/java/subway/integration/IntegrationTest.java delete mode 100644 src/test/java/subway/integration/LineIntegrationTest.java delete mode 100644 src/test/java/subway/integration/StationIntegrationTest.java create mode 100644 src/test/java/subway/repository/LineRepositoryTest.java create mode 100644 src/test/java/subway/repository/StationRepositoryTest.java create mode 100644 src/test/java/subway/service/LineServiceTest.java create mode 100644 src/test/java/subway/service/StationServiceTest.java create mode 100644 src/test/resources/application.yml create mode 100644 src/test/resources/data.sql create mode 100644 src/test/resources/logback-access.xml diff --git a/README.md b/README.md index 1a0f66ac2..5c77ec0da 100644 --- a/README.md +++ b/README.md @@ -1 +1,45 @@ -# jwp-subway-path \ No newline at end of file +# jwp-subway-path + +## API 문서 + +```text +http://localhost:8080/swagger-ui/index.html#/ +``` + +## 기능 요구사항 + +### 역 + +- [x] 역은 고유한 식별자를 가진다. +- [x] 역 이름은 `역`으로 끝나야 한다. +- [x] 역 이름은 2글자에서 11글자까지 가능하다. +- [x] 역 이름은 한글 + 숫자로만 이루어져야 한다. + +### 구역 + +- [x] 구역은 두 역과 역 사이의 거리를 가진다. +- [x] 거리는 양의 정수이고, 단위는 km이다. + +### 노선 + +- [x] 노선 이름은 숫자 + `호선` 이다. + - [x] 숫자는 1 ~ 9까지 가능하다. + +- [x] 노선의 색은 `색`으로 끝나야 한다. + - [x] 색 이름은 2글자에서 11글자까지 가능하다. + +- [x] 역을 추가할 수 있어야 한다. + -[x] 역을 추가할 때, 상행, 하행 역의 정보와 거리 정보를 입력받는다. + - 최초 등록이 아닐 경우, 상행역 또는 하행역 어느 한 가지도 존재하지 않으면 예외를 던진다. + -[x] 하나의 역은 여러 노선에 등록될 수 있다. + -[x] 두 역의 가운데에 다른 역을 등록할 때, 기존 거리를 고려해야 한다. + +- [x] 역이 2개 이상 등록된 노선을 전부 보여주어야 한다. + +- [x] 구역은 역 순서대로 저장되어 있어야 한다. + - [x] 노선 번호를 입력받으면, 노선에 포함된 역을 순서대로 보여주어야 한다. + +- [x] 역을 제거할 수 있어야 한다. + -[x] 역을 제거하면 남은 역을 재배치 해야 한다. + -[x] 노선에서 역이 제거되면 역과 역 사이의 거리도 재배정되어야 한다. + -[x] 노선에 등록된 역이 2개인 경우 하나의 역을 제거할 때 두 역이 모두 제거되어야 한다. diff --git a/build.gradle b/build.gradle index 68d8f2558..ebb673df5 100644 --- a/build.gradle +++ b/build.gradle @@ -13,9 +13,12 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' + implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' + testImplementation 'io.rest-assured:rest-assured:4.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' @@ -24,4 +27,4 @@ dependencies { test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/src/main/java/subway/SubwayApplication.java b/src/main/java/subway/SubwayApplication.java index 5174a4245..5780e8fb1 100644 --- a/src/main/java/subway/SubwayApplication.java +++ b/src/main/java/subway/SubwayApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class SubwayApplication { - public static void main(String[] args) { - SpringApplication.run(SubwayApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(SubwayApplication.class, args); + } } diff --git a/src/main/java/subway/application/LineService.java b/src/main/java/subway/application/LineService.java deleted file mode 100644 index bdb006f53..000000000 --- a/src/main/java/subway/application/LineService.java +++ /dev/null @@ -1,53 +0,0 @@ -package subway.application; - -import org.springframework.stereotype.Service; -import subway.dao.LineDao; -import subway.domain.Line; -import subway.dto.LineRequest; -import subway.dto.LineResponse; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class LineService { - private final LineDao lineDao; - - public LineService(LineDao lineDao) { - this.lineDao = lineDao; - } - - public LineResponse saveLine(LineRequest request) { - Line persistLine = lineDao.insert(new Line(request.getName(), request.getColor())); - return LineResponse.of(persistLine); - } - - public List findLineResponses() { - List persistLines = findLines(); - return persistLines.stream() - .map(LineResponse::of) - .collect(Collectors.toList()); - } - - public List findLines() { - return lineDao.findAll(); - } - - public LineResponse findLineResponseById(Long id) { - Line persistLine = findLineById(id); - return LineResponse.of(persistLine); - } - - public Line findLineById(Long id) { - return lineDao.findById(id); - } - - public void updateLine(Long id, LineRequest lineUpdateRequest) { - lineDao.update(new Line(id, lineUpdateRequest.getName(), lineUpdateRequest.getColor())); - } - - public void deleteLineById(Long id) { - lineDao.deleteById(id); - } - -} diff --git a/src/main/java/subway/application/StationService.java b/src/main/java/subway/application/StationService.java deleted file mode 100644 index 603d9daa7..000000000 --- a/src/main/java/subway/application/StationService.java +++ /dev/null @@ -1,44 +0,0 @@ -package subway.application; - -import org.springframework.stereotype.Service; -import subway.dao.StationDao; -import subway.domain.Station; -import subway.dto.StationRequest; -import subway.dto.StationResponse; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class StationService { - private final StationDao stationDao; - - public StationService(StationDao stationDao) { - this.stationDao = stationDao; - } - - public StationResponse saveStation(StationRequest stationRequest) { - Station station = stationDao.insert(new Station(stationRequest.getName())); - return StationResponse.of(station); - } - - public StationResponse findStationResponseById(Long id) { - return StationResponse.of(stationDao.findById(id)); - } - - public List findAllStationResponses() { - List stations = stationDao.findAll(); - - return stations.stream() - .map(StationResponse::of) - .collect(Collectors.toList()); - } - - public void updateStation(Long id, StationRequest stationRequest) { - stationDao.update(new Station(id, stationRequest.getName())); - } - - public void deleteStationById(Long id) { - stationDao.deleteById(id); - } -} \ No newline at end of file diff --git a/src/main/java/subway/config/SwaggerConfiguration.java b/src/main/java/subway/config/SwaggerConfiguration.java new file mode 100644 index 000000000..da242fdc9 --- /dev/null +++ b/src/main/java/subway/config/SwaggerConfiguration.java @@ -0,0 +1,22 @@ +package subway.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfiguration { + + @Bean + public OpenAPI openAPI() { + final Info info = new Info() + .title("지하철 API Document") + .version("v0.0.1") + .description("지하철 API 명세서입니다."); + return new OpenAPI() + .components(new Components()) + .info(info); + } +} diff --git a/src/main/java/subway/controller/ControllerAdvice.java b/src/main/java/subway/controller/ControllerAdvice.java new file mode 100644 index 000000000..81b3f69fb --- /dev/null +++ b/src/main/java/subway/controller/ControllerAdvice.java @@ -0,0 +1,58 @@ +package subway.controller; + +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import subway.exception.SubwayException; + +@RestControllerAdvice +public class ControllerAdvice { + + private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAdvice.class); + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(final Exception exception) { + final String message = exception.getMessage(); + LOGGER.error(message); + return ResponseEntity.internalServerError().body("알 수 없는 서버 에러가 발생했습니다."); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleException(final MethodArgumentNotValidException exception) { + final String message = exception.getBindingResult() + .getFieldErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(System.lineSeparator())); + LOGGER.warn(message); + return ResponseEntity.badRequest().body(message); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleException(final MethodArgumentTypeMismatchException exception) { + final String message = exception.getMessage(); + LOGGER.warn(message); + return ResponseEntity.badRequest().body(message); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleException(final MissingServletRequestParameterException exception) { + final String message = exception.getMessage(); + LOGGER.warn(message); + return ResponseEntity.badRequest().body(message); + } + + @ExceptionHandler(SubwayException.class) + public ResponseEntity handleException(final SubwayException exception) { + final String message = exception.getMessage(); + LOGGER.warn(message); + return ResponseEntity.badRequest().body(message); + } +} diff --git a/src/main/java/subway/controller/LineController.java b/src/main/java/subway/controller/LineController.java new file mode 100644 index 000000000..babdbf0d6 --- /dev/null +++ b/src/main/java/subway/controller/LineController.java @@ -0,0 +1,73 @@ +package subway.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.net.URI; +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import subway.controller.dto.request.LineCreateRequest; +import subway.controller.dto.request.SectionCreateRequest; +import subway.controller.dto.response.LineResponse; +import subway.controller.dto.response.LinesResponse; +import subway.service.LineService; + +@Tag(name = "Line", description = "노선 API Document") +@RequestMapping("/lines") +@RestController +public class LineController { + + private final LineService lineService; + + public LineController(final LineService lineService) { + this.lineService = lineService; + } + + @Operation(summary = "노선 추가 API", description = "새로운 노선을 추가합니다.") + @PostMapping + public ResponseEntity createLine(@Valid @RequestBody LineCreateRequest request) { + final Long lineId = lineService.createLine(request); + return ResponseEntity.created(URI.create("/lines/" + lineId)).build(); + } + + @Operation(summary = "노선 정보 조회 API", description = "노선의 정보를 조회합니다.") + @GetMapping("/{id}") + public ResponseEntity findLine(@PathVariable(name = "id") Long lineId) { + final LineResponse response = lineService.findLineById(lineId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "모든 노선 정보 조회 API", description = "모든 노선의 정보를 조회합니다.") + @GetMapping + public ResponseEntity findLines() { + final LinesResponse response = lineService.findLines(); + return ResponseEntity.ok(response); + } + + @Operation(summary = "노선 구간 등록 API", description = "노선에 새로운 구간을 등록합니다.") + @PostMapping("/{id}/sections") + public ResponseEntity createSection( + @PathVariable(name = "id") Long lineId, + @Valid @RequestBody SectionCreateRequest request + ) { + lineService.createSection(lineId, request); + return ResponseEntity.created(URI.create("/lines/" + lineId)).build(); + } + + @Operation(summary = "노선 구간 삭제 API", description = "노선의 특정 구간을 삭제합니다.") + @DeleteMapping("/{id}") + public ResponseEntity deleteSection( + @PathVariable(name = "id") Long lineId, + @RequestParam Long stationId + ) { + lineService.deleteStation(lineId, stationId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/subway/controller/StationController.java b/src/main/java/subway/controller/StationController.java new file mode 100644 index 000000000..6e08e5363 --- /dev/null +++ b/src/main/java/subway/controller/StationController.java @@ -0,0 +1,42 @@ +package subway.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.net.URI; +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import subway.controller.dto.request.StationCreateRequest; +import subway.controller.dto.response.StationResponse; +import subway.service.StationService; + +@Tag(name = "Station", description = "역 API Document") +@RequestMapping("/stations") +@RestController +public class StationController { + + private final StationService stationService; + + private StationController(final StationService stationService) { + this.stationService = stationService; + } + + @Operation(summary = "역 등록 API", description = "새로운 역을 등록합니다.") + @PostMapping + public ResponseEntity createStation(@Valid @RequestBody StationCreateRequest request) { + final Long stationId = stationService.createStation(request); + return ResponseEntity.created(URI.create("/stations/" + stationId)).build(); + } + + @Operation(summary = "역 정보 조회 API", description = "역 정보를 조회합니다.") + @GetMapping("/{id}") + public ResponseEntity findStationById(@PathVariable(name = "id") Long stationId) { + final StationResponse response = stationService.findStationById(stationId); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/subway/controller/dto/request/LineCreateRequest.java b/src/main/java/subway/controller/dto/request/LineCreateRequest.java new file mode 100644 index 000000000..f79e16c37 --- /dev/null +++ b/src/main/java/subway/controller/dto/request/LineCreateRequest.java @@ -0,0 +1,31 @@ +package subway.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; + +public class LineCreateRequest { + + @Schema(description = "노선 이름") + @NotBlank(message = "노선 이름은 공백일 수 없습니다.") + private String name; + + @Schema(description = "노선 색") + @NotBlank(message = "노선 색깔은 공백일 수 없습니다.") + private String color; + + private LineCreateRequest() { + } + + public LineCreateRequest(final String name, final String color) { + this.name = name; + this.color = color; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } +} diff --git a/src/main/java/subway/controller/dto/request/SectionCreateRequest.java b/src/main/java/subway/controller/dto/request/SectionCreateRequest.java new file mode 100644 index 000000000..db27cce27 --- /dev/null +++ b/src/main/java/subway/controller/dto/request/SectionCreateRequest.java @@ -0,0 +1,42 @@ +package subway.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; + +public class SectionCreateRequest { + + @Schema(description = "상행 역 ID") + @NotNull(message = "상행 역 ID는 존재해야 합니다.") + private Long upwardStationId; + + @Schema(description = "하행 역 ID") + @NotNull(message = "하행 역 ID는 존재해야 합니다.") + private Long downwardStationId; + + @Schema(description = "상행 역, 하행 역 사이 거리") + @NotNull(message = "역 간의 거리는 존재해야 합니다.") + @Positive(message = "역 간의 거리는 0보다 커야합니다.") + private Integer distance; + + private SectionCreateRequest() { + } + + public SectionCreateRequest(final Long upwardStationId, final Long downwardStationId, final Integer distance) { + this.upwardStationId = upwardStationId; + this.downwardStationId = downwardStationId; + this.distance = distance; + } + + public Long getUpwardStationId() { + return upwardStationId; + } + + public Long getDownwardStationId() { + return downwardStationId; + } + + public Integer getDistance() { + return distance; + } +} diff --git a/src/main/java/subway/controller/dto/request/StationCreateRequest.java b/src/main/java/subway/controller/dto/request/StationCreateRequest.java new file mode 100644 index 000000000..9cfca2943 --- /dev/null +++ b/src/main/java/subway/controller/dto/request/StationCreateRequest.java @@ -0,0 +1,22 @@ +package subway.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotBlank; + +public class StationCreateRequest { + + @Schema(description = "역 이름") + @NotBlank(message = "역 이름은 공백일 수 없습니다.") + private String name; + + private StationCreateRequest() { + } + + public StationCreateRequest(final String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/subway/controller/dto/response/LineResponse.java b/src/main/java/subway/controller/dto/response/LineResponse.java new file mode 100644 index 000000000..857205826 --- /dev/null +++ b/src/main/java/subway/controller/dto/response/LineResponse.java @@ -0,0 +1,55 @@ +package subway.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.stream.Collectors; +import subway.domain.line.Line; +import subway.domain.station.Station; + +public class LineResponse { + + @Schema(description = "노선 ID") + private Long id; + + @Schema(description = "노선 이름") + private String name; + + @Schema(description = "노선 색") + private String color; + + @Schema(description = "노선의 역 목록") + private List stations; + + public LineResponse(final Long id, final String name, final String color, final List stations) { + this.id = id; + this.name = name; + this.color = color; + this.stations = stations; + } + + public static LineResponse from(final Line line) { + return new LineResponse(line.getId(), line.getName(), line.getColor(), generateStations(line.getStations())); + } + + private static List generateStations(final List stations) { + return stations.stream() + .map(StationResponse::from) + .collect(Collectors.toUnmodifiableList()); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public List getStations() { + return stations; + } +} diff --git a/src/main/java/subway/controller/dto/response/LinesResponse.java b/src/main/java/subway/controller/dto/response/LinesResponse.java new file mode 100644 index 000000000..9a2876933 --- /dev/null +++ b/src/main/java/subway/controller/dto/response/LinesResponse.java @@ -0,0 +1,18 @@ +package subway.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public class LinesResponse { + + @Schema(description = "노선 목록") + private List lines; + + public LinesResponse(final List lines) { + this.lines = lines; + } + + public List getLines() { + return lines; + } +} diff --git a/src/main/java/subway/controller/dto/response/StationResponse.java b/src/main/java/subway/controller/dto/response/StationResponse.java new file mode 100644 index 000000000..3175c95f5 --- /dev/null +++ b/src/main/java/subway/controller/dto/response/StationResponse.java @@ -0,0 +1,30 @@ +package subway.controller.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import subway.domain.station.Station; + +public class StationResponse { + + @Schema(description = "역 ID") + private Long id; + + @Schema(description = "역 이름") + private String name; + + public StationResponse(final Long id, final String name) { + this.id = id; + this.name = name; + } + + public static StationResponse from(final Station station) { + return new StationResponse(station.getId(), station.getName()); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/subway/dao/LineDao.java b/src/main/java/subway/dao/LineDao.java index f644bac29..1bd20ff55 100644 --- a/src/main/java/subway/dao/LineDao.java +++ b/src/main/java/subway/dao/LineDao.java @@ -1,61 +1,63 @@ package subway.dao; +import java.util.List; +import java.util.Optional; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; -import subway.domain.Line; - -import javax.sql.DataSource; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import subway.entity.LineEntity; @Repository public class LineDao { - private final JdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert insertAction; - private RowMapper rowMapper = (rs, rowNum) -> - new Line( - rs.getLong("id"), - rs.getString("name"), - rs.getString("color") - ); + private static final RowMapper ROW_MAPPER = (rs, rowNum) -> new LineEntity( + rs.getLong("id"), + rs.getString("name"), + rs.getString("color") + ); - public LineDao(JdbcTemplate jdbcTemplate, DataSource dataSource) { + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert jdbcInsert; + + public LineDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; - this.insertAction = new SimpleJdbcInsert(dataSource) + this.jdbcInsert = new SimpleJdbcInsert(jdbcTemplate) .withTableName("line") + .usingColumns("name", "color") .usingGeneratedKeyColumns("id"); } - public Line insert(Line line) { - Map params = new HashMap<>(); - params.put("id", line.getId()); - params.put("name", line.getName()); - params.put("color", line.getColor()); - - Long lineId = insertAction.executeAndReturnKey(params).longValue(); - return new Line(lineId, line.getName(), line.getColor()); + public LineEntity save(final LineEntity lineEntity) { + final SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(lineEntity); + final Long lineId = jdbcInsert.executeAndReturnKey(parameterSource).longValue(); + return new LineEntity(lineId, lineEntity.getName(), lineEntity.getColor()); } - public List findAll() { - String sql = "select id, name, color from LINE"; - return jdbcTemplate.query(sql, rowMapper); - } - - public Line findById(Long id) { - String sql = "select id, name, color from LINE WHERE id = ?"; - return jdbcTemplate.queryForObject(sql, rowMapper, id); + public Optional findById(final Long lineId) { + final String sql = "SELECT id, name, color FROM line WHERE id = ?"; + try { + final LineEntity result = jdbcTemplate.queryForObject( + sql, + ROW_MAPPER, + lineId + ); + return Optional.ofNullable(result); + } catch (EmptyResultDataAccessException exception) { + return Optional.empty(); + } } - public void update(Line newLine) { - String sql = "update LINE set name = ?, color = ? where id = ?"; - jdbcTemplate.update(sql, new Object[]{newLine.getName(), newLine.getColor(), newLine.getId()}); + public List findAll() { + final String sql = "SELECT id, name, color FROM line"; + return jdbcTemplate.query(sql, ROW_MAPPER); } - public void deleteById(Long id) { - jdbcTemplate.update("delete from Line where id = ?", id); + public int update(final LineEntity lineEntity) { + final String sql = "UPDATE line SET name = ?, color = ? WHERE id = ?"; + return jdbcTemplate.update(sql, lineEntity.getName(), lineEntity.getColor(), lineEntity.getId()); } } diff --git a/src/main/java/subway/dao/SectionDao.java b/src/main/java/subway/dao/SectionDao.java new file mode 100644 index 000000000..53583e956 --- /dev/null +++ b/src/main/java/subway/dao/SectionDao.java @@ -0,0 +1,81 @@ +package subway.dao; + +import java.util.List; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import subway.entity.SectionEntity; + +@Repository +public class SectionDao { + + private static final RowMapper ROW_MAPPER = (rs, count) -> new SectionEntity( + rs.getLong("id"), + rs.getLong("line_id"), + rs.getLong("upward_station_id"), + rs.getString("upward_station_name"), + rs.getLong("downward_station_id"), + rs.getString("downward_station_name"), + rs.getInt("distance") + ); + + private final NamedParameterJdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert jdbcInsert; + + public SectionDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + this.jdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("section") + .usingColumns("line_id", "upward_station_id", "downward_station_id", "distance") + .usingGeneratedKeyColumns("id"); + } + + public SectionEntity save(final SectionEntity sectionEntity) { + final SqlParameterSource sqlParameterSource = new BeanPropertySqlParameterSource(sectionEntity); + final Long sectionId = jdbcInsert.executeAndReturnKey(sqlParameterSource).longValue(); + final String sql = "SELECT s.id AS id," + + " s.line_id AS line_id," + + " us.id AS upward_station_id," + + " us.name AS upward_station_name," + + " ds.id AS downward_station_id," + + " ds.name AS downward_station_name," + + " s.distance AS distance" + + " FROM section s" + + " JOIN station us ON s.upward_station_id = us.id" + + " JOIN station ds ON s.downward_station_id = ds.id" + + " WHERE s.id = ?"; + return jdbcTemplate.getJdbcOperations().queryForObject(sql, ROW_MAPPER, sectionId); + } + + public void saveAll(final List sectionEntities) { + final String sql = "INSERT INTO section (line_id, upward_station_id, downward_station_id, distance)" + + " VALUES (:lineId, :upwardStationId, :downwardStationId, :distance)"; + jdbcTemplate.batchUpdate(sql, SqlParameterSourceUtils.createBatch(sectionEntities)); + } + + public List findAllByLineId(final Long lineId) { + final String sql = "SELECT s.id AS id," + + " s.line_id AS line_id," + + " us.id AS upward_station_id," + + " us.name AS upward_station_name," + + " ds.id AS downward_station_id," + + " ds.name AS downward_station_name," + + " s.distance AS distance" + + " FROM section s" + + " JOIN station us ON s.upward_station_id = us.id" + + " JOIN station ds ON s.downward_station_id = ds.id" + + " WHERE s.line_id = ?" + + " ORDER BY s.id"; + return jdbcTemplate.getJdbcOperations().query(sql, ROW_MAPPER, lineId); + } + + public void deleteAllByLineId(final Long lineId) { + final String sql = "DELETE FROM section WHERE line_id = ?"; + jdbcTemplate.getJdbcOperations().update(sql, lineId); + } +} diff --git a/src/main/java/subway/dao/StationDao.java b/src/main/java/subway/dao/StationDao.java index 07f7eab30..16f9206ca 100644 --- a/src/main/java/subway/dao/StationDao.java +++ b/src/main/java/subway/dao/StationDao.java @@ -1,58 +1,47 @@ package subway.dao; +import java.util.Optional; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; -import subway.domain.Station; - -import javax.sql.DataSource; -import java.util.List; +import subway.entity.StationEntity; @Repository public class StationDao { - private final JdbcTemplate jdbcTemplate; - private final SimpleJdbcInsert insertAction; - - private RowMapper rowMapper = (rs, rowNum) -> - new Station( - rs.getLong("id"), - rs.getString("name") - ); + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert jdbcInsert; - public StationDao(JdbcTemplate jdbcTemplate, DataSource dataSource) { + public StationDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; - this.insertAction = new SimpleJdbcInsert(dataSource) + jdbcInsert = new SimpleJdbcInsert(jdbcTemplate) .withTableName("station") - .usingGeneratedKeyColumns("id"); - } - - public Station insert(Station station) { - SqlParameterSource params = new BeanPropertySqlParameterSource(station); - Long id = insertAction.executeAndReturnKey(params).longValue(); - return new Station(id, station.getName()); - } - - public List findAll() { - String sql = "select * from STATION"; - return jdbcTemplate.query(sql, rowMapper); - } - - public Station findById(Long id) { - String sql = "select * from STATION where id = ?"; - return jdbcTemplate.queryForObject(sql, rowMapper, id); + .usingGeneratedKeyColumns("id") + .usingColumns("name"); } - public void update(Station newStation) { - String sql = "update STATION set name = ? where id = ?"; - jdbcTemplate.update(sql, new Object[]{newStation.getName(), newStation.getId()}); + public StationEntity save(final StationEntity stationEntity) { + final SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(stationEntity); + final Long stationId = jdbcInsert.executeAndReturnKey(parameterSource).longValue(); + return new StationEntity(stationId, stationEntity.getName()); } - public void deleteById(Long id) { - String sql = "delete from STATION where id = ?"; - jdbcTemplate.update(sql, id); + public Optional findById(final Long stationId) { + final String sql = "SELECT id, name FROM station WHERE id = ?"; + try { + final StationEntity result = jdbcTemplate.queryForObject( + sql, + (rs, rowNum) -> new StationEntity( + rs.getLong("id"), + rs.getString("name") + ), + stationId); + return Optional.ofNullable(result); + } catch (EmptyResultDataAccessException exception) { + return Optional.empty(); + } } } diff --git a/src/main/java/subway/domain/Line.java b/src/main/java/subway/domain/Line.java deleted file mode 100644 index 699d0b6df..000000000 --- a/src/main/java/subway/domain/Line.java +++ /dev/null @@ -1,48 +0,0 @@ -package subway.domain; - -import java.util.Objects; - -public class Line { - private Long id; - private String name; - private String color; - - public Line() { - } - - public Line(String name, String color) { - this.name = name; - this.color = color; - } - - public Line(Long id, String name, String color) { - this.id = id; - this.name = name; - this.color = color; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getColor() { - return color; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Line line = (Line) o; - return Objects.equals(id, line.id) && Objects.equals(name, line.name) && Objects.equals(color, line.color); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, color); - } -} diff --git a/src/main/java/subway/domain/Station.java b/src/main/java/subway/domain/Station.java deleted file mode 100644 index dbf9d7835..000000000 --- a/src/main/java/subway/domain/Station.java +++ /dev/null @@ -1,41 +0,0 @@ -package subway.domain; - -import java.util.Objects; - -public class Station { - private Long id; - private String name; - - public Station() { - } - - public Station(Long id, String name) { - this.id = id; - this.name = name; - } - - public Station(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Station station = (Station) o; - return id.equals(station.id) && name.equals(station.name); - } - - @Override - public int hashCode() { - return Objects.hash(id, name); - } -} diff --git a/src/main/java/subway/domain/line/Color.java b/src/main/java/subway/domain/line/Color.java new file mode 100644 index 000000000..5abc814ce --- /dev/null +++ b/src/main/java/subway/domain/line/Color.java @@ -0,0 +1,34 @@ +package subway.domain.line; + +import java.util.Objects; +import java.util.regex.Pattern; +import subway.exception.InvalidColorException; + +final class Color { + + private static final int MAXIMUM_LENGTH = 11; + private static final Pattern PATTERN = Pattern.compile("^[가-힣]+색$"); + + private final String value; + + public Color(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (Objects.isNull(value) || value.isBlank()) { + throw new InvalidColorException("노선 색은 존재해야 합니다."); + } + if (value.length() > MAXIMUM_LENGTH) { + throw new InvalidColorException("노선 색은 " + MAXIMUM_LENGTH + "글자까지 가능합니다."); + } + if (!PATTERN.matcher(value).matches()) { + throw new InvalidColorException("색 이름은 한글로 이루어져 있고, '색'으로 끝나야 합니다."); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/subway/domain/line/Line.java b/src/main/java/subway/domain/line/Line.java new file mode 100644 index 000000000..29b5d3e70 --- /dev/null +++ b/src/main/java/subway/domain/line/Line.java @@ -0,0 +1,178 @@ +package subway.domain.line; + +import java.util.LinkedList; +import java.util.List; +import subway.domain.section.Section; +import subway.domain.section.Sections; +import subway.domain.station.Station; +import subway.exception.InvalidDistanceException; +import subway.exception.InvalidSectionException; + +public final class Line { + + private static final int ADDITIONAL_INDEX = -1; + private static final int INITIAL_SECTION_SIZE = 2; + + private final Long id; + private final Name name; + private final Color color; + private final Sections sections; + + public Line(final String name, final String color) { + this(null, name, color); + } + + public Line(final Long id, final String name, final String color) { + this(id, name, color, new LinkedList<>()); + } + + public Line(final Long id, final String name, final String color, final List
sections) { + this.id = id; + this.name = new Name(name); + this.color = new Color(color); + this.sections = new Sections(sections); + } + + public void addSection(final Station upward, final Station downward, final int distance) { + if (sections.isEmpty()) { + addInitialSection(upward, downward, distance); + return; + } + + final int upwardPosition = sections.findPosition(upward); + final int downwardPosition = sections.findPosition(downward); + validateForAddSection(upwardPosition, downwardPosition); + + if (shouldAdd(upwardPosition)) { + if (isFirstSection(downwardPosition)) { + sections.add(0, new Section(upward, downward, distance)); + return; + } + addUpwardSectionBetweenStations(upward, downward, distance, downwardPosition); + return; + } + + if (isLastSection(upwardPosition)) { + addDownwardSectionInLast(upward, downward, distance); + return; + } + addDownwardSectionBetweenStations(upward, downward, distance, upwardPosition); + } + + private void addInitialSection(final Station upward, final Station downward, final int distance) { + sections.add(new Section(upward, downward, distance)); + sections.add(new Section(downward, Station.TERMINAL, 0)); + } + + private void validateForAddSection(final int upwardPosition, final int downwardPosition) { + if (upwardPosition != Sections.NOT_EXIST_INDEX && downwardPosition != Sections.NOT_EXIST_INDEX) { + throw new InvalidSectionException("두 역이 이미 노선에 존재합니다."); + } + if (upwardPosition == Sections.NOT_EXIST_INDEX && downwardPosition == Sections.NOT_EXIST_INDEX) { + throw new InvalidSectionException("연결할 역 정보가 없습니다."); + } + } + + private boolean shouldAdd(final int position) { + return position == ADDITIONAL_INDEX; + } + + private boolean isFirstSection(final int position) { + return position == 0; + } + + private void addUpwardSectionBetweenStations( + final Station upward, + final Station downward, + final int distance, + final int downwardPosition + ) { + final int targetPosition = downwardPosition - 1; + final Section section = sections.findSectionByPosition(targetPosition); + sections.deleteByPosition(targetPosition); + validateDistance(section.getDistance(), distance); + sections.add(targetPosition, new Section(upward, downward, distance)); + sections.add(targetPosition, new Section(section.getUpward(), upward, section.getDistance() - distance)); + } + + private boolean isLastSection(final int position) { + return sections.size() - 1 == position; + } + + private void addDownwardSectionInLast(final Station upward, final Station downward, final int distance) { + sections.deleteByPosition(sections.size() - 1); + sections.add(sections.size(), new Section(upward, downward, distance)); + sections.add(sections.size(), new Section(downward, Station.TERMINAL, 0)); + } + + private void addDownwardSectionBetweenStations( + final Station upward, + final Station downward, + final int distance, + final int position + ) { + final Section section = sections.findSectionByPosition(position); + sections.deleteByPosition(position); + validateDistance(section.getDistance(), distance); + sections.add(position, new Section(downward, section.getDownward(), section.getDistance() - distance)); + sections.add(position, new Section(upward, downward, distance)); + } + + private void validateDistance(final int oldDistance, final int inputDistance) { + if (oldDistance <= inputDistance) { + throw new InvalidDistanceException("추가될 역의 거리는 추가될 위치의 두 역사이의 거리보다 작아야합니다."); + } + } + + public void deleteStation(final Station station) { + final int position = sections.findPosition(station); + if (position == Sections.NOT_EXIST_INDEX) { + throw new InvalidSectionException("노선에 해당 역이 존재하지 않습니다."); + } + + if (sections.size() == INITIAL_SECTION_SIZE) { + sections.clear(); + return; + } + + if (position == 0) { + sections.deleteByPosition(position); + return; + } + + final Section targetSection = sections.findSectionByPosition(position); + final Section previousSection = sections.findSectionByPosition(position - 1); + + sections.deleteByPosition(position - 1); + sections.deleteByPosition(position - 1); + + sections.add( + position - 1, + new Section( + previousSection.getUpward(), + targetSection.getDownward(), + targetSection.getDistance() + previousSection.getDistance() + ) + ); + } + + public Long getId() { + return id; + } + + public String getName() { + return name.getValue(); + } + + public String getColor() { + return color.getValue(); + } + + public List getStations() { + return sections.getUpwards(); + } + + public List
getSections() { + return sections.getValue(); + } +} diff --git a/src/main/java/subway/domain/line/Name.java b/src/main/java/subway/domain/line/Name.java new file mode 100644 index 000000000..c44bc6f61 --- /dev/null +++ b/src/main/java/subway/domain/line/Name.java @@ -0,0 +1,30 @@ +package subway.domain.line; + +import java.util.Objects; +import java.util.regex.Pattern; +import subway.exception.InvalidLineNameException; + +final class Name { + + private static final Pattern PATTERN = Pattern.compile("^[1-9]호선$"); + + private final String value; + + public Name(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (Objects.isNull(value) || value.isBlank()) { + throw new InvalidLineNameException("노선 이름은 존재해야 합니다."); + } + if (!PATTERN.matcher(value).matches()) { + throw new InvalidLineNameException("노선 이름은 1~9호선이어야 합니다."); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/subway/domain/section/Distance.java b/src/main/java/subway/domain/section/Distance.java new file mode 100644 index 000000000..79d1f107d --- /dev/null +++ b/src/main/java/subway/domain/section/Distance.java @@ -0,0 +1,25 @@ +package subway.domain.section; + +import subway.exception.InvalidDistanceException; + +final class Distance { + + private static final int MINIMUM_VALUE = 0; + + private final int value; + + public Distance(final int value) { + validate(value); + this.value = value; + } + + private void validate(final int value) { + if (value < MINIMUM_VALUE) { + throw new InvalidDistanceException("역 사이의 거리는 0이상이어야합니다."); + } + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/subway/domain/section/Section.java b/src/main/java/subway/domain/section/Section.java new file mode 100644 index 000000000..f6f22eab9 --- /dev/null +++ b/src/main/java/subway/domain/section/Section.java @@ -0,0 +1,35 @@ +package subway.domain.section; + +import subway.domain.station.Station; +import subway.entity.SectionEntity; + +public final class Section { + + private final Station upward; + private final Station downward; + private final Distance distance; + + public Section(final Station upward, final Station downward, final int distance) { + this.upward = upward; + this.downward = downward; + this.distance = new Distance(distance); + } + + public static Section from(final SectionEntity sectionEntity) { + final Station upward = new Station(sectionEntity.getUpwardStationId(), sectionEntity.getUpwardStation()); + final Station downward = new Station(sectionEntity.getDownwardStationId(), sectionEntity.getDownwardStation()); + return new Section(upward, downward, sectionEntity.getDistance()); + } + + public Station getUpward() { + return upward; + } + + public Station getDownward() { + return downward; + } + + public int getDistance() { + return distance.getValue(); + } +} diff --git a/src/main/java/subway/domain/section/Sections.java b/src/main/java/subway/domain/section/Sections.java new file mode 100644 index 000000000..2c02aeece --- /dev/null +++ b/src/main/java/subway/domain/section/Sections.java @@ -0,0 +1,63 @@ +package subway.domain.section; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import subway.domain.station.Station; + +public final class Sections { + + public static final int NOT_EXIST_INDEX = -1; + + private final List
sections; + + public Sections(final List
sections) { + this.sections = sections; + } + + public void add(final Section section) { + sections.add(section); + } + + public void add(final int position, final Section section) { + sections.add(position, section); + } + + public Section findSectionByPosition(final int position) { + return sections.get(position); + } + + public void deleteByPosition(final int position) { + sections.remove(position); + } + + public int findPosition(final Station station) { + try { + return getUpwards().indexOf(station); + } catch (NullPointerException exception) { + return NOT_EXIST_INDEX; + } + } + + public boolean isEmpty() { + return sections.isEmpty(); + } + + public int size() { + return sections.size(); + } + + public void clear() { + sections.clear(); + } + + public List getUpwards() { + return sections.stream() + .map(Section::getUpward) + .collect(Collectors.toList()); + } + + public List
getValue() { + return new LinkedList<>(sections); + } +} diff --git a/src/main/java/subway/domain/station/Name.java b/src/main/java/subway/domain/station/Name.java new file mode 100644 index 000000000..e61a3470a --- /dev/null +++ b/src/main/java/subway/domain/station/Name.java @@ -0,0 +1,34 @@ +package subway.domain.station; + +import java.util.Objects; +import java.util.regex.Pattern; +import subway.exception.InvalidStationNameException; + +final class Name { + + private static final int MAXIMUM_LENGTH = 11; + private static final Pattern PATTERN = Pattern.compile("^[가-힣0-9]+역$"); + + private final String value; + + public Name(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (Objects.isNull(value) || value.isBlank()) { + throw new InvalidStationNameException("역 이름은 공백일 수 없습니다."); + } + if (value.length() > MAXIMUM_LENGTH) { + throw new InvalidStationNameException("역 이름은 " + MAXIMUM_LENGTH + "글자까지 가능합니다."); + } + if (!PATTERN.matcher(value).matches()) { + throw new InvalidStationNameException("역 이름은 한글, 숫자만 가능하고, '역'으로 끝나야 합니다."); + } + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/subway/domain/station/Station.java b/src/main/java/subway/domain/station/Station.java new file mode 100644 index 000000000..77dd378d9 --- /dev/null +++ b/src/main/java/subway/domain/station/Station.java @@ -0,0 +1,45 @@ +package subway.domain.station; + +import java.util.Objects; + +public final class Station { + + public static final Station TERMINAL = new Station(0L, "종착역"); + + private final Long id; + private final Name name; + + public Station(final String name) { + this(null, name); + } + + public Station(final Long id, final String name) { + this.id = id; + this.name = new Name(name); + } + + public Long getId() { + return id; + } + + public String getName() { + return name.getValue(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Station station = (Station) o; + return Objects.equals(id, station.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/subway/dto/LineRequest.java b/src/main/java/subway/dto/LineRequest.java deleted file mode 100644 index 16cb5bf76..000000000 --- a/src/main/java/subway/dto/LineRequest.java +++ /dev/null @@ -1,23 +0,0 @@ -package subway.dto; - -public class LineRequest { - private String name; - private String color; - - public LineRequest() { - } - - public LineRequest(String name, String color) { - this.name = name; - this.color = color; - } - - public String getName() { - return name; - } - - public String getColor() { - return color; - } - -} diff --git a/src/main/java/subway/dto/LineResponse.java b/src/main/java/subway/dto/LineResponse.java deleted file mode 100644 index c9b668122..000000000 --- a/src/main/java/subway/dto/LineResponse.java +++ /dev/null @@ -1,31 +0,0 @@ -package subway.dto; - -import subway.domain.Line; - -public class LineResponse { - private Long id; - private String name; - private String color; - - public LineResponse(Long id, String name, String color) { - this.id = id; - this.name = name; - this.color = color; - } - - public static LineResponse of(Line line) { - return new LineResponse(line.getId(), line.getName(), line.getColor()); - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getColor() { - return color; - } -} diff --git a/src/main/java/subway/dto/StationRequest.java b/src/main/java/subway/dto/StationRequest.java deleted file mode 100644 index 15175303d..000000000 --- a/src/main/java/subway/dto/StationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package subway.dto; - -public class StationRequest { - private String name; - - public StationRequest() { - } - - public StationRequest(String name) { - this.name = name; - } - - public String getName() { - return name; - } -} diff --git a/src/main/java/subway/dto/StationResponse.java b/src/main/java/subway/dto/StationResponse.java deleted file mode 100644 index 5eec02fe0..000000000 --- a/src/main/java/subway/dto/StationResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package subway.dto; - -import subway.domain.Station; - -public class StationResponse { - private Long id; - private String name; - - public StationResponse(Long id, String name) { - this.id = id; - this.name = name; - } - - public static StationResponse of(Station station) { - return new StationResponse(station.getId(), station.getName()); - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } -} diff --git a/src/main/java/subway/entity/LineEntity.java b/src/main/java/subway/entity/LineEntity.java new file mode 100644 index 000000000..bc54e8618 --- /dev/null +++ b/src/main/java/subway/entity/LineEntity.java @@ -0,0 +1,54 @@ +package subway.entity; + +import java.util.Objects; +import subway.domain.line.Line; + +public class LineEntity { + + private final Long id; + private final String name; + private final String color; + + public LineEntity(final String name, final String color) { + this(null, name, color); + } + + public LineEntity(final Long id, final String name, final String color) { + this.id = id; + this.name = name; + this.color = color; + } + + public static LineEntity from(final Line line) { + return new LineEntity(line.getId(), line.getName(), line.getColor()); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final LineEntity that = (LineEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/subway/entity/SectionEntity.java b/src/main/java/subway/entity/SectionEntity.java new file mode 100644 index 000000000..cb9437030 --- /dev/null +++ b/src/main/java/subway/entity/SectionEntity.java @@ -0,0 +1,91 @@ +package subway.entity; + +import java.util.Objects; +import subway.domain.section.Section; + +public class SectionEntity { + + private final Long id; + private final Long lineId; + private final Long upwardStationId; + private final String upwardStation; + private final Long downwardStationId; + private final String downwardStation; + private final Integer distance; + + public SectionEntity( + final Long lineId, + final Long upwardStationId, + final Long downwardStationId, + final Integer distance + ) { + this(null, lineId, upwardStationId, null, downwardStationId, null, distance); + } + + public SectionEntity( + final Long id, final Long lineId, final Long upwardStationId, final String upwardStation, + final Long downwardStationId, final String downwardStation, final Integer distance + ) { + this.id = id; + this.lineId = lineId; + this.upwardStationId = upwardStationId; + this.upwardStation = upwardStation; + this.downwardStationId = downwardStationId; + this.downwardStation = downwardStation; + this.distance = distance; + } + + public static SectionEntity of(final Long lineId, final Section section) { + return new SectionEntity( + lineId, + section.getUpward().getId(), + section.getDownward().getId(), + section.getDistance() + ); + } + + public Long getId() { + return id; + } + + public Long getLineId() { + return lineId; + } + + public Long getUpwardStationId() { + return upwardStationId; + } + + public String getUpwardStation() { + return upwardStation; + } + + public Long getDownwardStationId() { + return downwardStationId; + } + + public String getDownwardStation() { + return downwardStation; + } + + public Integer getDistance() { + return distance; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SectionEntity that = (SectionEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/subway/entity/StationEntity.java b/src/main/java/subway/entity/StationEntity.java new file mode 100644 index 000000000..d622fd32f --- /dev/null +++ b/src/main/java/subway/entity/StationEntity.java @@ -0,0 +1,48 @@ +package subway.entity; + +import java.util.Objects; +import subway.domain.station.Station; + +public class StationEntity { + + private final Long id; + private final String name; + + public StationEntity(final String name) { + this(null, name); + } + + public StationEntity(final Long id, final String name) { + this.id = id; + this.name = name; + } + + public static StationEntity from(final Station station) { + return new StationEntity(station.getId(), station.getName()); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final StationEntity that = (StationEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/subway/exception/InvalidColorException.java b/src/main/java/subway/exception/InvalidColorException.java new file mode 100644 index 000000000..8bdfdea62 --- /dev/null +++ b/src/main/java/subway/exception/InvalidColorException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public final class InvalidColorException extends SubwayException { + + public InvalidColorException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/exception/InvalidDistanceException.java b/src/main/java/subway/exception/InvalidDistanceException.java new file mode 100644 index 000000000..76f4ee13b --- /dev/null +++ b/src/main/java/subway/exception/InvalidDistanceException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public final class InvalidDistanceException extends SubwayException { + + public InvalidDistanceException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/exception/InvalidLineException.java b/src/main/java/subway/exception/InvalidLineException.java new file mode 100644 index 000000000..6b17e3ad9 --- /dev/null +++ b/src/main/java/subway/exception/InvalidLineException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class InvalidLineException extends SubwayException { + + public InvalidLineException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/exception/InvalidLineNameException.java b/src/main/java/subway/exception/InvalidLineNameException.java new file mode 100644 index 000000000..6d2ec852a --- /dev/null +++ b/src/main/java/subway/exception/InvalidLineNameException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public final class InvalidLineNameException extends SubwayException { + + public InvalidLineNameException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/exception/InvalidSectionException.java b/src/main/java/subway/exception/InvalidSectionException.java new file mode 100644 index 000000000..1e955dac7 --- /dev/null +++ b/src/main/java/subway/exception/InvalidSectionException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class InvalidSectionException extends SubwayException { + + public InvalidSectionException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/exception/InvalidStationException.java b/src/main/java/subway/exception/InvalidStationException.java new file mode 100644 index 000000000..1ce9fd4bc --- /dev/null +++ b/src/main/java/subway/exception/InvalidStationException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class InvalidStationException extends SubwayException { + + public InvalidStationException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/exception/InvalidStationNameException.java b/src/main/java/subway/exception/InvalidStationNameException.java new file mode 100644 index 000000000..4e092c62a --- /dev/null +++ b/src/main/java/subway/exception/InvalidStationNameException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public final class InvalidStationNameException extends SubwayException { + + public InvalidStationNameException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/exception/SubwayException.java b/src/main/java/subway/exception/SubwayException.java new file mode 100644 index 000000000..6c6023e1b --- /dev/null +++ b/src/main/java/subway/exception/SubwayException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class SubwayException extends RuntimeException { + + public SubwayException(final String message) { + super(message); + } +} diff --git a/src/main/java/subway/repository/LineRepository.java b/src/main/java/subway/repository/LineRepository.java new file mode 100644 index 000000000..fa39fcdbd --- /dev/null +++ b/src/main/java/subway/repository/LineRepository.java @@ -0,0 +1,83 @@ +package subway.repository; + +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Repository; +import subway.dao.LineDao; +import subway.dao.SectionDao; +import subway.domain.line.Line; +import subway.domain.section.Section; +import subway.domain.station.Station; +import subway.entity.LineEntity; +import subway.entity.SectionEntity; +import subway.exception.InvalidLineException; +import subway.exception.InvalidSectionException; + +@Repository +public class LineRepository { + + private final LineDao lineDao; + private final SectionDao sectionDao; + + public LineRepository(final LineDao lineDao, final SectionDao sectionDao) { + this.lineDao = lineDao; + this.sectionDao = sectionDao; + } + + public Line save(final Line line) { + final LineEntity lineEntity = lineDao.save(LineEntity.from(line)); + return new Line(lineEntity.getId(), lineEntity.getName(), lineEntity.getColor()); + } + + public Line findById(final Long lineId) { + final LineEntity lineEntity = lineDao.findById(lineId) + .orElseThrow(() -> new InvalidLineException("존재하지 않는 노선 ID 입니다.")); + final List sectionEntities = sectionDao.findAllByLineId(lineId); + return generateLine(lineEntity, sectionEntities); + } + + private Line generateLine(final LineEntity lineEntity, final List sectionEntities) { + final Line line = new Line(lineEntity.getId(), lineEntity.getName(), lineEntity.getColor()); + loadSections(line, generateSections(sectionEntities)); + return line; + } + + private List
generateSections(final List sectionEntities) { + return sectionEntities.stream() + .map(Section::from) + .collect(Collectors.toList()); + } + + private void loadSections(final Line line, final List
sections) { + while (!sections.isEmpty()) { + final Section section = sections.remove(0); + try { + line.addSection(section.getUpward(), section.getDownward(), section.getDistance()); + } catch (InvalidSectionException e) { + sections.add(section); + } + } + } + + public List findAll() { + return lineDao.findAll() + .stream() + .map(lineEntity -> generateLine(lineEntity, sectionDao.findAllByLineId(lineEntity.getId()))) + .collect(Collectors.toList()); + } + + public void update(final Line line) { + lineDao.update(LineEntity.from(line)); + sectionDao.deleteAllByLineId(line.getId()); + final List entities = generateSectionEntities(line); + sectionDao.saveAll(entities); + } + + private List generateSectionEntities(final Line line) { + final List
sections = line.getSections(); + sections.removeIf(section -> section.getDownward() == Station.TERMINAL); + return sections.stream() + .map(section -> SectionEntity.of(line.getId(), section)) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/src/main/java/subway/repository/StationRepository.java b/src/main/java/subway/repository/StationRepository.java new file mode 100644 index 000000000..505476bbe --- /dev/null +++ b/src/main/java/subway/repository/StationRepository.java @@ -0,0 +1,28 @@ +package subway.repository; + +import org.springframework.stereotype.Repository; +import subway.dao.StationDao; +import subway.domain.station.Station; +import subway.entity.StationEntity; +import subway.exception.InvalidStationException; + +@Repository +public class StationRepository { + + private final StationDao stationDao; + + public StationRepository(final StationDao stationDao) { + this.stationDao = stationDao; + } + + public Station save(final Station station) { + final StationEntity stationEntity = stationDao.save(StationEntity.from(station)); + return new Station(stationEntity.getId(), stationEntity.getName()); + } + + public Station findById(final Long stationId) { + final StationEntity stationEntity = stationDao.findById(stationId) + .orElseThrow(() -> new InvalidStationException("존재하지 않은 역 ID입니다.")); + return new Station(stationEntity.getId(), stationEntity.getName()); + } +} diff --git a/src/main/java/subway/service/LineService.java b/src/main/java/subway/service/LineService.java new file mode 100644 index 000000000..8030386e6 --- /dev/null +++ b/src/main/java/subway/service/LineService.java @@ -0,0 +1,66 @@ +package subway.service; + +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import subway.controller.dto.request.LineCreateRequest; +import subway.controller.dto.request.SectionCreateRequest; +import subway.controller.dto.response.LineResponse; +import subway.controller.dto.response.LinesResponse; +import subway.domain.line.Line; +import subway.domain.station.Station; +import subway.repository.LineRepository; +import subway.repository.StationRepository; + +@Service +@Transactional(readOnly = true) +public class LineService { + + private final LineRepository lineRepository; + private final StationRepository stationRepository; + + public LineService(final LineRepository lineRepository, final StationRepository stationRepository) { + this.lineRepository = lineRepository; + this.stationRepository = stationRepository; + } + + @Transactional + public Long createLine(final LineCreateRequest request) { + final Line line = new Line(request.getName(), request.getColor()); + return lineRepository.save(line).getId(); + } + + public LineResponse findLineById(final Long lineId) { + final Line line = lineRepository.findById(lineId); + return LineResponse.from(line); + } + + public LinesResponse findLines() { + final List lines = lineRepository.findAll(); + return new LinesResponse(generateLineResponses(lines)); + } + + private List generateLineResponses(final List lines) { + return lines.stream() + .map(LineResponse::from) + .collect(Collectors.toUnmodifiableList()); + } + + @Transactional + public void createSection(final Long lineId, final SectionCreateRequest request) { + final Line line = lineRepository.findById(lineId); + final Station upward = stationRepository.findById(request.getUpwardStationId()); + final Station downward = stationRepository.findById(request.getDownwardStationId()); + line.addSection(upward, downward, request.getDistance()); + lineRepository.update(line); + } + + @Transactional + public void deleteStation(final Long lineId, final Long stationId) { + final Line line = lineRepository.findById(lineId); + final Station station = stationRepository.findById(stationId); + line.deleteStation(station); + lineRepository.update(line); + } +} diff --git a/src/main/java/subway/service/StationService.java b/src/main/java/subway/service/StationService.java new file mode 100644 index 000000000..8ef4077a2 --- /dev/null +++ b/src/main/java/subway/service/StationService.java @@ -0,0 +1,30 @@ +package subway.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import subway.controller.dto.request.StationCreateRequest; +import subway.controller.dto.response.StationResponse; +import subway.domain.station.Station; +import subway.repository.StationRepository; + +@Service +@Transactional(readOnly = true) +public class StationService { + + private final StationRepository stationRepository; + + public StationService(final StationRepository stationRepository) { + this.stationRepository = stationRepository; + } + + @Transactional + public Long createStation(final StationCreateRequest request) { + final Station station = new Station(request.getName()); + return stationRepository.save(station).getId(); + } + + public StationResponse findStationById(final Long stationId) { + final Station station = stationRepository.findById(stationId); + return StationResponse.from(station); + } +} diff --git a/src/main/java/subway/ui/LineController.java b/src/main/java/subway/ui/LineController.java deleted file mode 100644 index 3a335ee14..000000000 --- a/src/main/java/subway/ui/LineController.java +++ /dev/null @@ -1,55 +0,0 @@ -package subway.ui; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import subway.application.LineService; -import subway.dto.LineRequest; -import subway.dto.LineResponse; - -import java.net.URI; -import java.sql.SQLException; -import java.util.List; - -@RestController -@RequestMapping("/lines") -public class LineController { - - private final LineService lineService; - - public LineController(LineService lineService) { - this.lineService = lineService; - } - - @PostMapping - public ResponseEntity createLine(@RequestBody LineRequest lineRequest) { - LineResponse line = lineService.saveLine(lineRequest); - return ResponseEntity.created(URI.create("/lines/" + line.getId())).body(line); - } - - @GetMapping - public ResponseEntity> findAllLines() { - return ResponseEntity.ok(lineService.findLineResponses()); - } - - @GetMapping("/{id}") - public ResponseEntity findLineById(@PathVariable Long id) { - return ResponseEntity.ok(lineService.findLineResponseById(id)); - } - - @PutMapping("/{id}") - public ResponseEntity updateLine(@PathVariable Long id, @RequestBody LineRequest lineUpdateRequest) { - lineService.updateLine(id, lineUpdateRequest); - return ResponseEntity.ok().build(); - } - - @DeleteMapping("/{id}") - public ResponseEntity deleteLine(@PathVariable Long id) { - lineService.deleteLineById(id); - return ResponseEntity.noContent().build(); - } - - @ExceptionHandler(SQLException.class) - public ResponseEntity handleSQLException() { - return ResponseEntity.badRequest().build(); - } -} diff --git a/src/main/java/subway/ui/StationController.java b/src/main/java/subway/ui/StationController.java deleted file mode 100644 index 5bf52a9a9..000000000 --- a/src/main/java/subway/ui/StationController.java +++ /dev/null @@ -1,54 +0,0 @@ -package subway.ui; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import subway.dto.StationRequest; -import subway.dto.StationResponse; -import subway.application.StationService; - -import java.net.URI; -import java.sql.SQLException; -import java.util.List; - -@RestController -@RequestMapping("/stations") -public class StationController { - private final StationService stationService; - - public StationController(StationService stationService) { - this.stationService = stationService; - } - - @PostMapping - public ResponseEntity createStation(@RequestBody StationRequest stationRequest) { - StationResponse station = stationService.saveStation(stationRequest); - return ResponseEntity.created(URI.create("/stations/" + station.getId())).body(station); - } - - @GetMapping - public ResponseEntity> showStations() { - return ResponseEntity.ok().body(stationService.findAllStationResponses()); - } - - @GetMapping("/{id}") - public ResponseEntity showStation(@PathVariable Long id) { - return ResponseEntity.ok().body(stationService.findStationResponseById(id)); - } - - @PutMapping("/{id}") - public ResponseEntity updateStation(@PathVariable Long id, @RequestBody StationRequest stationRequest) { - stationService.updateStation(id, stationRequest); - return ResponseEntity.ok().build(); - } - - @DeleteMapping("/{id}") - public ResponseEntity deleteStation(@PathVariable Long id) { - stationService.deleteStationById(id); - return ResponseEntity.noContent().build(); - } - - @ExceptionHandler(SQLException.class) - public ResponseEntity handleSQLException() { - return ResponseEntity.badRequest().build(); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..70bb8d36b --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,28 @@ +spring: + h2: + console: + enabled: true + + datasource: + url: jdbc:h2:mem:testdb;MODE=MySQL + driver-class-name: org.h2.Driver + + sql: + init: + schema-locations: classpath:data.sql + data-locations: classpath:dummy.sql + + springdoc: + packages-to-scan: subway + default-consumes-media-type: application/json;charset=UTF-8 + default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + path: subway.html + tags-sorter: alpha + operations-sorter: alpha + api-docs: + path: /subway-api-docs + groups: + enabled: true + cache: + disabled: true diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..a7a4c1c3f --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS STATION +( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(15) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS LINE +( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(15) NOT NULL, + color VARCHAR(15) NOT NULL, + PRIMARY KEY (id) + ); + +CREATE TABLE IF NOT EXISTS SECTION +( + id BIGINT AUTO_INCREMENT NOT NULL, + line_id BIGINT NOT NULL, + upward_station_id BIGINT NOT NULL, + downward_station_id BIGINT NOT NULL, + distance INT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (line_id) REFERENCES LINE (id), + FOREIGN KEY (upward_station_id) REFERENCES STATION (id), + FOREIGN KEY (downward_station_id) REFERENCES STATION (id) +); diff --git a/src/main/resources/dummy.sql b/src/main/resources/dummy.sql new file mode 100644 index 000000000..983f7cd46 --- /dev/null +++ b/src/main/resources/dummy.sql @@ -0,0 +1,5 @@ + +INSERT INTO line (id, name, color) VALUES (1, '2호선', '초록색'); +INSERT INTO station (id, name) VALUES (1, '잠실역'); +INSERT INTO station (id, name) VALUES (2, '잠실새내역'); +INSERT INTO section (id, line_id, upward_station_id, downward_station_id, distance) VALUES (1, 1, 1, 2, 10); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index fc6de5f5c..000000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists STATION -( - id bigint auto_increment not null, - name varchar(255) not null unique, - primary key(id) -); - -create table if not exists LINE -( - id bigint auto_increment not null, - name varchar(255) not null unique, - color varchar(20) not null, - primary key(id) -); diff --git a/src/test/java/subway/Integration/IntegrationTest.java b/src/test/java/subway/Integration/IntegrationTest.java new file mode 100644 index 000000000..0a449c801 --- /dev/null +++ b/src/test/java/subway/Integration/IntegrationTest.java @@ -0,0 +1,20 @@ +package subway.Integration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc +public class IntegrationTest { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; +} diff --git a/src/test/java/subway/Integration/LineControllerIntegrationTest.java b/src/test/java/subway/Integration/LineControllerIntegrationTest.java new file mode 100644 index 000000000..42318c757 --- /dev/null +++ b/src/test/java/subway/Integration/LineControllerIntegrationTest.java @@ -0,0 +1,261 @@ +package subway.Integration; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import subway.controller.dto.request.LineCreateRequest; +import subway.controller.dto.request.SectionCreateRequest; +import subway.dao.StationDao; +import subway.domain.line.Line; +import subway.domain.station.Station; +import subway.entity.StationEntity; +import subway.repository.LineRepository; +import subway.repository.StationRepository; + +public class LineControllerIntegrationTest extends IntegrationTest { + + @Autowired + private StationDao stationDao; + + @Autowired + private LineRepository lineRepository; + + @Autowired + private StationRepository stationRepository; + + private Line lineTwo; + private Station upward; + private Station downward; + + @BeforeEach + void setUp() { + lineTwo = lineRepository.save(new Line("2호선", "초록색")); + upward = stationRepository.save(new Station("잠실역")); + downward = stationRepository.save(new Station("잠실새내역")); + lineTwo.addSection(upward, downward, 10); + lineRepository.update(lineTwo); + } + + @Test + @DisplayName("노선 목록을 조회한다.") + void findLines() throws Exception { + final Line lineFour = lineRepository.save(new Line("4호선", "하늘색")); + final Station lineFourUpward = stationRepository.save(new Station("이수역")); + final Station lineFourDownward = stationRepository.save(new Station("서울역")); + lineFour.addSection(lineFourUpward, lineFourDownward, 10); + lineRepository.update(lineFour); + + mockMvc.perform(get("/lines")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lines", hasSize(2))) + .andExpect(jsonPath("$.lines[0].id").value(lineTwo.getId())) + .andExpect(jsonPath("$.lines[0].name").value("2호선")) + .andExpect(jsonPath("$.lines[0].color").value("초록색")) + .andExpect(jsonPath("$.lines[0].stations", hasSize(2))) + .andExpect(jsonPath("$.lines[0].stations[0].id").value(upward.getId())) + .andExpect(jsonPath("$.lines[0].stations[0].name").value("잠실역")) + .andExpect(jsonPath("$.lines[0].stations[1].id").value(downward.getId())) + .andExpect(jsonPath("$.lines[0].stations[1].name").value("잠실새내역")) + .andExpect(jsonPath("$.lines[1].id").value(lineFour.getId())) + .andExpect(jsonPath("$.lines[1].name").value("4호선")) + .andExpect(jsonPath("$.lines[1].color").value("하늘색")) + .andExpect(jsonPath("$.lines[1].stations", hasSize(2))) + .andExpect(jsonPath("$.lines[1].stations[0].id").value(lineFourUpward.getId())) + .andExpect(jsonPath("$.lines[1].stations[0].name").value("이수역")) + .andExpect(jsonPath("$.lines[1].stations[1].id").value(lineFourDownward.getId())) + .andExpect(jsonPath("$.lines[1].stations[1].name").value("서울역")); + } + + @Nested + @DisplayName("노선 생성 요청시 ") + class CreateLine { + + @Test + @DisplayName("유효한 노선 정보라면 새로운 노선을 추가한다.") + void createLine() throws Exception { + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색"); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.LOCATION, containsString("/lines/"))); + } + + @Test + @DisplayName("이름이 공백이라면 400 상태를 반환한다.") + void createLineWithInvalidName() throws Exception { + final LineCreateRequest request = new LineCreateRequest(" ", "초록색"); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("노선 이름은 공백일 수 없습니다.")); + } + + @Test + @DisplayName("색이 공백이라면 400 상태를 반환한다.") + void createLineWithInvalidColor() throws Exception { + final LineCreateRequest request = new LineCreateRequest("2호선", " "); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("노선 색깔은 공백일 수 없습니다.")); + } + } + + @Nested + @DisplayName("노선 조회 시 ") + class FindLine { + + @Test + @DisplayName("존재하는 노선이라면 노선 정보를 조회한다.") + void findLine() throws Exception { + mockMvc.perform(get("/lines/{id}", lineTwo.getId())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(lineTwo.getId())) + .andExpect(jsonPath("$.name").value("2호선")) + .andExpect(jsonPath("$.color").value("초록색")) + .andExpect(jsonPath("$.stations", hasSize(2))) + .andExpect(jsonPath("$.stations[0].id").value(upward.getId())) + .andExpect(jsonPath("$.stations[0].name").value("잠실역")) + .andExpect(jsonPath("$.stations[1].id").value(downward.getId())) + .andExpect(jsonPath("$.stations[1].name").value("잠실새내역")); + } + + @Test + @DisplayName("ID로 변환할 수 없는 타입이라면 400 상태를 반환한다.") + void findLineWithInvalidIDType() throws Exception { + mockMvc.perform(get("/lines/{id}", "l")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("노선에 역을 등록할 시 ") + class CreateSection { + + @Test + @DisplayName("유효한 정보가 입력되면 노선에 역을 등록한다.") + void createSection() throws Exception { + final StationEntity middle = stationDao.save(new StationEntity("종합운동장역")); + final SectionCreateRequest request = new SectionCreateRequest(upward.getId(), middle.getId(), 5); + + mockMvc.perform(post("/lines/{id}/sections", lineTwo.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.LOCATION, containsString("/lines/" + lineTwo.getId()))); + } + + @Test + @DisplayName("상행 역 ID가 입력되지 않으면 400 상태를 반환한다.") + void createSectionWithoutUpwardStationId() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(null, downward.getId(), 10); + + mockMvc.perform(post("/lines/{id}/sections", lineTwo.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("상행 역 ID는 존재해야 합니다.")); + } + + @Test + @DisplayName("하행 역 ID가 입력되지 않으면 400 상태를 반환한다.") + void createSectionWithoutDownwardStationId() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(upward.getId(), null, 10); + + mockMvc.perform(post("/lines/{id}/sections", lineTwo.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("하행 역 ID는 존재해야 합니다.")); + } + + @Test + @DisplayName("역 간의 거리가 입력되지 않으면 400 상태를 반환한다.") + void createSectionWithoutDistance() throws Exception { + final StationEntity middle = stationDao.save(new StationEntity("종합운동장역")); + final SectionCreateRequest request = new SectionCreateRequest(upward.getId(), middle.getId(), null); + + mockMvc.perform(post("/lines/{id}/sections", lineTwo.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 간의 거리는 존재해야 합니다.")); + } + + @Test + @DisplayName("역 간의 거리가 0이하이면 400 상태를 반환한다.") + void createSectionWithNegativeDistance() throws Exception { + final StationEntity middle = stationDao.save(new StationEntity("종합운동장역")); + final SectionCreateRequest request = new SectionCreateRequest(upward.getId(), middle.getId(), -1); + + mockMvc.perform(post("/lines/{id}/sections", lineTwo.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 간의 거리는 0보다 커야합니다.")); + } + } + + @Nested + @DisplayName("노선에서 역 삭제 요청 시") + class DeleteStation { + + @Test + @DisplayName("유효한 요청이라면 역을 삭제한다.") + void deleteStation() throws Exception { + mockMvc.perform(delete("/lines/{lineId}", lineTwo.getId()) + .queryParam("stationId", String.valueOf(upward.getId()))) + .andDo(print()) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("역 아이디가 존재하지 않으면 400 상태를 반환한다.") + void deleteStationWithoutStationId() throws Exception { + mockMvc.perform(delete("/lines/{lineId}", Long.MAX_VALUE)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("역 아이디로 변환할 수 없는 타입이면 400 상태를 반환한다.") + void deleteStationWithInvalidStationIDType() throws Exception { + mockMvc.perform(delete("/lines/{lineId}", lineTwo.getId()) + .queryParam("stationId", "s")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/src/test/java/subway/Integration/StationControllerIntegrationTest.java b/src/test/java/subway/Integration/StationControllerIntegrationTest.java new file mode 100644 index 000000000..1e9959d13 --- /dev/null +++ b/src/test/java/subway/Integration/StationControllerIntegrationTest.java @@ -0,0 +1,88 @@ +package subway.Integration; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import subway.controller.dto.request.StationCreateRequest; +import subway.domain.station.Station; +import subway.repository.StationRepository; + +class StationControllerIntegrationTest extends IntegrationTest { + + @Autowired + private StationRepository stationRepository; + + @Nested + @DisplayName("역 추가 요청시 ") + class CreateStation { + + @Test + @DisplayName("유효한 역 정보라면 새로운 역을 추가한다") + void createStation() throws Exception { + final StationCreateRequest request = new StationCreateRequest("잠실역"); + + mockMvc.perform(post("/stations") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.LOCATION, containsString("/stations/"))); + } + + @Test + @DisplayName("역 이름이 잘못되면 400 상태를 반환한다.") + void createStationWithInvalidName() throws Exception { + final StationCreateRequest request = new StationCreateRequest(" "); + + mockMvc.perform(post("/stations") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 이름은 공백일 수 없습니다.")); + } + } + + @Nested + @DisplayName("역 정보 조회 시 ") + class FindStation { + + private Station station; + + @BeforeEach + void setUp() { + station = stationRepository.save(new Station("잠실역")); + } + + @Test + @DisplayName("유효한 ID라면 역 정보를 조회한다.") + void findStation() throws Exception { + mockMvc.perform(get("/stations/{id}", station.getId())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(station.getId())) + .andExpect(jsonPath("$.name").value(station.getName())); + } + + @Test + @DisplayName("ID가 유효하지 않다면 400 상태를 반환한다.") + void findStationWithInvalidID() throws Exception { + mockMvc.perform(get("/stations/{id}", "poi")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/src/test/java/subway/SubwayApplicationTests.java b/src/test/java/subway/SubwayApplicationTests.java index cdf84476f..92a2eb3b8 100644 --- a/src/test/java/subway/SubwayApplicationTests.java +++ b/src/test/java/subway/SubwayApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class SubwayApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/src/test/java/subway/controller/LineControllerTest.java b/src/test/java/subway/controller/LineControllerTest.java new file mode 100644 index 000000000..a8ed6663b --- /dev/null +++ b/src/test/java/subway/controller/LineControllerTest.java @@ -0,0 +1,275 @@ +package subway.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import subway.controller.dto.request.LineCreateRequest; +import subway.controller.dto.request.SectionCreateRequest; +import subway.controller.dto.response.LineResponse; +import subway.controller.dto.response.LinesResponse; +import subway.controller.dto.response.StationResponse; +import subway.service.LineService; + +@WebMvcTest(LineController.class) +class LineControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private LineService lineService; + + @Test + @DisplayName("노선 목록을 조회한다.") + void findLines() throws Exception { + final List stationsOfLineTwo = List.of( + new StationResponse(1L, "잠실역"), + new StationResponse(2L, "잠실새내역") + ); + final List stationsOfLineFour = List.of( + new StationResponse(3L, "이수역"), + new StationResponse(4L, "서울역") + ); + final List lines = List.of( + new LineResponse(1L, "2호선", "초록색", stationsOfLineTwo), + new LineResponse(2L, "4호선", "하늘색", stationsOfLineFour)); + final LinesResponse response = new LinesResponse(lines); + + given(lineService.findLines()).willReturn(response); + + mockMvc.perform(get("/lines")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lines", hasSize(2))) + .andExpect(jsonPath("$.lines[0].id").value(1)) + .andExpect(jsonPath("$.lines[0].name").value("2호선")) + .andExpect(jsonPath("$.lines[0].color").value("초록색")) + .andExpect(jsonPath("$.lines[0].stations", hasSize(2))) + .andExpect(jsonPath("$.lines[0].stations[0].id").value(1)) + .andExpect(jsonPath("$.lines[0].stations[0].name").value("잠실역")) + .andExpect(jsonPath("$.lines[0].stations[1].id").value(2)) + .andExpect(jsonPath("$.lines[0].stations[1].name").value("잠실새내역")) + .andExpect(jsonPath("$.lines[1].id").value(2)) + .andExpect(jsonPath("$.lines[1].name").value("4호선")) + .andExpect(jsonPath("$.lines[1].color").value("하늘색")) + .andExpect(jsonPath("$.lines[1].stations", hasSize(2))) + .andExpect(jsonPath("$.lines[1].stations[0].id").value(3)) + .andExpect(jsonPath("$.lines[1].stations[0].name").value("이수역")) + .andExpect(jsonPath("$.lines[1].stations[1].id").value(4)) + .andExpect(jsonPath("$.lines[1].stations[1].name").value("서울역")); + } + + @Nested + @DisplayName("노선 생성 요청시 ") + class CreateLine { + + @Test + @DisplayName("유효한 노선 정보라면 새로운 노선을 추가한다.") + void createLine() throws Exception { + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색"); + + given(lineService.createLine(any(LineCreateRequest.class))).willReturn(1L); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.LOCATION, containsString("/lines/1"))); + } + + @Test + @DisplayName("이름이 공백이라면 400 상태를 반환한다.") + void createLineWithInvalidName() throws Exception { + final LineCreateRequest request = new LineCreateRequest(" ", "초록색"); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("노선 이름은 공백일 수 없습니다.")); + } + + @Test + @DisplayName("색이 공백이라면 400 상태를 반환한다.") + void createLineWithInvalidColor() throws Exception { + final LineCreateRequest request = new LineCreateRequest("2호선", " "); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("노선 색깔은 공백일 수 없습니다.")); + } + } + + @Nested + @DisplayName("노선 조회 시 ") + class FindLine { + + @Test + @DisplayName("존재하는 노선이라면 노선 정보를 조회한다.") + void findLine() throws Exception { + final List stations = List.of( + new StationResponse(1L, "잠실역"), + new StationResponse(2L, "잠실새내역") + ); + final LineResponse response = new LineResponse(1L, "2호선", "초록색", stations); + + given(lineService.findLineById(1L)).willReturn(response); + + mockMvc.perform(get("/lines/{id}", 1L)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("2호선")) + .andExpect(jsonPath("$.color").value("초록색")) + .andExpect(jsonPath("$.stations", hasSize(2))) + .andExpect(jsonPath("$.stations[0].id").value(1)) + .andExpect(jsonPath("$.stations[0].name").value("잠실역")) + .andExpect(jsonPath("$.stations[1].id").value("2")) + .andExpect(jsonPath("$.stations[1].name").value("잠실새내역")); + } + + @Test + @DisplayName("ID로 변환할 수 없는 타입이라면 400 상태를 반환한다.") + void findLineWithInvalidIDType() throws Exception { + mockMvc.perform(get("/lines/{id}", "l")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("노선에 역을 등록할 시 ") + class CreateSection { + + @Test + @DisplayName("유효한 정보가 입력되면 노선에 역을 등록한다.") + void createSection() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(1L, 2L, 10); + + willDoNothing().given(lineService).createSection(1L, mock(SectionCreateRequest.class)); + + mockMvc.perform(post("/lines/{id}/sections", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.LOCATION, containsString("/lines/1"))); + } + + @Test + @DisplayName("상행 역 ID가 입력되지 않으면 400 상태를 반환한다.") + void createSectionWithoutUpwardStationId() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(null, 2L, 10); + + mockMvc.perform(post("/lines/{id}/sections", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("상행 역 ID는 존재해야 합니다.")); + } + + @Test + @DisplayName("하행 역 ID가 입력되지 않으면 400 상태를 반환한다.") + void createSectionWithoutDownwardStationId() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(1L, null, 10); + + mockMvc.perform(post("/lines/{id}/sections", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("하행 역 ID는 존재해야 합니다.")); + } + + @Test + @DisplayName("역 간의 거리가 입력되지 않으면 400 상태를 반환한다.") + void createSectionWithoutDistance() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(1L, 2L, null); + + mockMvc.perform(post("/lines/{id}/sections", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 간의 거리는 존재해야 합니다.")); + } + + @Test + @DisplayName("역 간의 거리가 0이하이면 400 상태를 반환한다.") + void createSectionWithNegativeDistance() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(1L, 2L, -1); + + mockMvc.perform(post("/lines/{id}/sections", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 간의 거리는 0보다 커야합니다.")); + } + } + + @Nested + @DisplayName("노선에서 역 삭제 요청 시") + class DeleteStation { + + @Test + @DisplayName("유효한 요청이라면 역을 삭제한다.") + void deleteStation() throws Exception { + willDoNothing().given(lineService).deleteStation(any(Long.class), any(Long.class)); + + mockMvc.perform(delete("/lines/{lineId}", 1L) + .queryParam("stationId", String.valueOf(1L))) + .andDo(print()) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("역 아이디가 존재하지 않으면 400 상태를 반환한다.") + void deleteStationWithoutStationId() throws Exception { + mockMvc.perform(delete("/lines/{lineId}", 1L)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("역 아이디로 변환할 수 없는 타입이면 400 상태를 반환한다.") + void deleteStationWithInvalidStationIDType() throws Exception { + mockMvc.perform(delete("/lines/{lineId}", 1L) + .queryParam("stationId", "s")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/src/test/java/subway/controller/StationControllerTest.java b/src/test/java/subway/controller/StationControllerTest.java new file mode 100644 index 000000000..d3f26e045 --- /dev/null +++ b/src/test/java/subway/controller/StationControllerTest.java @@ -0,0 +1,99 @@ +package subway.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import subway.controller.dto.request.StationCreateRequest; +import subway.controller.dto.response.StationResponse; +import subway.service.StationService; + +@WebMvcTest(StationController.class) +class StationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private StationService stationService; + + @Nested + @DisplayName("역 추가 요청시 ") + class CreateStation { + + @Test + @DisplayName("유효한 역 정보라면 새로운 역을 추가한다") + void createStation() throws Exception { + final StationCreateRequest request = new StationCreateRequest("잠실역"); + + given(stationService.createStation(any(StationCreateRequest.class))).willReturn(1L); + + mockMvc.perform(post("/stations") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.LOCATION, containsString("/stations/"))); + } + + @Test + @DisplayName("역 이름이 잘못되면 400 상태를 반환한다.") + void createStationWithInvalidName() throws Exception { + final StationCreateRequest request = new StationCreateRequest(" "); + + mockMvc.perform(post("/stations") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 이름은 공백일 수 없습니다.")); + } + } + + @Nested + @DisplayName("역 정보 조회 시 ") + class FindStation { + + @Test + @DisplayName("유효한 ID라면 역 정보를 조회한다.") + void findStation() throws Exception { + final StationResponse response = new StationResponse(1L, "잠실역"); + + given(stationService.findStationById(1L)).willReturn(response); + + mockMvc.perform(get("/stations/{id}", 1L)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.name").value("잠실역")); + } + + @Test + @DisplayName("ID가 유효하지 않다면 400 상태를 반환한다.") + void findStationWithInvalidID() throws Exception { + mockMvc.perform(get("/stations/{id}", "poi")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/src/test/java/subway/dao/LineDaoTest.java b/src/test/java/subway/dao/LineDaoTest.java new file mode 100644 index 000000000..454158d6d --- /dev/null +++ b/src/test/java/subway/dao/LineDaoTest.java @@ -0,0 +1,103 @@ +package subway.dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import subway.entity.LineEntity; + +@JdbcTest +class LineDaoTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + private LineDao lineDao; + + @BeforeEach + void setUp() { + lineDao = new LineDao(jdbcTemplate); + } + + @Test + @DisplayName("모든 노선을 조회한다.") + void findAll() { + final LineEntity lineTwo = lineDao.save(new LineEntity("2호선", "초록색")); + final LineEntity lineFour = lineDao.save(new LineEntity("4호선", "하늘색")); + + final List lines = lineDao.findAll(); + + assertAll( + () -> assertThat(lines).hasSize(2), + () -> assertThat(lines.get(0).getId()).isEqualTo(lineTwo.getId()), + () -> assertThat(lines.get(0).getName()).isEqualTo(lineTwo.getName()), + () -> assertThat(lines.get(0).getColor()).isEqualTo(lineTwo.getColor()), + () -> assertThat(lines.get(1).getId()).isEqualTo(lineFour.getId()), + () -> assertThat(lines.get(1).getName()).isEqualTo(lineFour.getName()), + () -> assertThat(lines.get(1).getColor()).isEqualTo(lineFour.getColor()) + ); + } + + @Nested + @DisplayName("아이디로 조회시 ") + class FindById { + + @Test + @DisplayName("존재하는 ID라면 노선 정보를 반환한다.") + void findById() { + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + + final Optional line = lineDao.findById(lineEntity.getId()); + + assertAll( + () -> assertThat(line.get().getId()).isEqualTo(lineEntity.getId()), + () -> assertThat(line.get().getName()).isEqualTo(lineEntity.getName()), + () -> assertThat(line.get().getColor()).isEqualTo(lineEntity.getColor()) + ); + } + + @Test + @DisplayName("존재하지 않는 ID라면 빈 값을 반환한다.") + void findByWithInvalidId() { + final Optional line = lineDao.findById(-3L); + + assertThat(line).isEmpty(); + } + } + + @Nested + @DisplayName("노선 정보 업데이트시 ") + class Update { + + @Test + @DisplayName("존재하는 노선이라면 정보를 업데이트한다.") + void update() { + final LineEntity line = lineDao.save(new LineEntity("2호선", "초록색")); + + final int numberOfUpdatedRow = lineDao.update(new LineEntity(line.getId(), "4호선", "하늘색")); + + final LineEntity updatedLine = lineDao.findById(line.getId()).get(); + assertAll( + () -> assertThat(numberOfUpdatedRow).isEqualTo(1), + () -> assertThat(updatedLine.getName()).isEqualTo("4호선"), + () -> assertThat(updatedLine.getColor()).isEqualTo("하늘색") + ); + } + + @Test + @DisplayName("존재하지 않는 노선이라면 0을 반환한다.") + void updateWithNotExistLine() { + final int numberOfUpdatedRow = lineDao.update(new LineEntity(1L, "4호선", "하늘색")); + + assertThat(numberOfUpdatedRow).isEqualTo(0); + } + } +} diff --git a/src/test/java/subway/dao/SectionDaoTest.java b/src/test/java/subway/dao/SectionDaoTest.java new file mode 100644 index 000000000..5892e9758 --- /dev/null +++ b/src/test/java/subway/dao/SectionDaoTest.java @@ -0,0 +1,94 @@ +package subway.dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import subway.entity.LineEntity; +import subway.entity.SectionEntity; +import subway.entity.StationEntity; + +@JdbcTest +class SectionDaoTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + private SectionDao sectionDao; + private StationDao stationDao; + private LineDao lineDao; + + @BeforeEach + void setUp() { + sectionDao = new SectionDao(jdbcTemplate); + stationDao = new StationDao(jdbcTemplate); + lineDao = new LineDao(jdbcTemplate); + } + + @Test + @DisplayName("해당 노선의 모든 구간 정보를 조회한다.") + void findAllByLineId() { + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + final StationEntity upward = stationDao.save(new StationEntity("잠실역")); + final StationEntity downward = stationDao.save(new StationEntity("잠실새내역")); + final SectionEntity entity = new SectionEntity(lineEntity.getId(), upward.getId(), downward.getId(), 10); + final SectionEntity savedEntity = sectionDao.save(entity); + + final List sections = sectionDao.findAllByLineId(savedEntity.getLineId()); + + assertThat(sections).containsExactly(savedEntity); + } + + @Test + @DisplayName("모든 구간 정보를 저장한다.") + void saveAll() { + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + final StationEntity upward = stationDao.save(new StationEntity("잠실역")); + final StationEntity middle = stationDao.save(new StationEntity("잠실새내역")); + final StationEntity downward = stationDao.save(new StationEntity("종합운동장역")); + final List sections = List.of( + new SectionEntity(lineEntity.getId(), upward.getId(), middle.getId(), 10), + new SectionEntity(lineEntity.getId(), middle.getId(), downward.getId(), 10) + ); + + sectionDao.saveAll(sections); + + final List result = sectionDao.findAllByLineId(lineEntity.getId()); + assertAll( + () -> assertThat(result).hasSize(2), + () -> assertThat(result.get(0).getLineId()).isEqualTo(lineEntity.getId()), + () -> assertThat(result.get(0).getUpwardStationId()).isEqualTo(upward.getId()), + () -> assertThat(result.get(0).getUpwardStation()).isEqualTo(upward.getName()), + () -> assertThat(result.get(0).getDownwardStationId()).isEqualTo(middle.getId()), + () -> assertThat(result.get(0).getDownwardStation()).isEqualTo(middle.getName()), + () -> assertThat(result.get(0).getDistance()).isEqualTo(10), + () -> assertThat(result.get(1).getLineId()).isEqualTo(lineEntity.getId()), + () -> assertThat(result.get(1).getUpwardStationId()).isEqualTo(middle.getId()), + () -> assertThat(result.get(1).getUpwardStation()).isEqualTo(middle.getName()), + () -> assertThat(result.get(1).getDownwardStationId()).isEqualTo(downward.getId()), + () -> assertThat(result.get(1).getDownwardStation()).isEqualTo(downward.getName()), + () -> assertThat(result.get(1).getDistance()).isEqualTo(10) + ); + } + + @Test + @DisplayName("노선의 구간 정보를 삭제한다.") + void deleteAllByLineId() { + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + final StationEntity upward = stationDao.save(new StationEntity("잠실역")); + final StationEntity downward = stationDao.save(new StationEntity("잠실새내역")); + final SectionEntity entity = new SectionEntity(lineEntity.getId(), upward.getId(), downward.getId(), 10); + final SectionEntity savedEntity = sectionDao.save(entity); + + sectionDao.deleteAllByLineId(savedEntity.getLineId()); + + final List sectionEntities = sectionDao.findAllByLineId(savedEntity.getLineId()); + assertThat(sectionEntities).isEmpty(); + } +} diff --git a/src/test/java/subway/dao/StationDaoTest.java b/src/test/java/subway/dao/StationDaoTest.java new file mode 100644 index 000000000..0863a86e8 --- /dev/null +++ b/src/test/java/subway/dao/StationDaoTest.java @@ -0,0 +1,54 @@ +package subway.dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import subway.entity.StationEntity; + +@JdbcTest +class StationDaoTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + private StationDao stationDao; + + @BeforeEach + void setUp() { + stationDao = new StationDao(jdbcTemplate); + } + + @Nested + @DisplayName("아이디로 조회시 ") + class FindById { + + @Test + @DisplayName("존재하는 ID라면 역 정보를 반환한다.") + void findById() { + final StationEntity stationEntity = stationDao.save(new StationEntity("잠실역")); + + final Optional station = stationDao.findById(stationEntity.getId()); + + assertAll( + () -> assertThat(station.get().getId()).isEqualTo(stationEntity.getId()), + () -> assertThat(station.get().getName()).isEqualTo(stationEntity.getName()) + ); + } + + @Test + @DisplayName("존재하지 않는 ID라면 빈 값을 반환한다.") + void findByWithInvalidId() { + final Optional station = stationDao.findById(-3L); + + assertThat(station).isEmpty(); + } + } +} diff --git a/src/test/java/subway/domain/line/ColorTest.java b/src/test/java/subway/domain/line/ColorTest.java new file mode 100644 index 000000000..5bab2ece7 --- /dev/null +++ b/src/test/java/subway/domain/line/ColorTest.java @@ -0,0 +1,41 @@ +package subway.domain.line; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import subway.exception.InvalidColorException; + +class ColorTest { + + @ParameterizedTest + @DisplayName("색 이름이 존재하지 않으면 예외를 던진다.") + @NullAndEmptySource + void validateWithNull(final String input) { + assertThatThrownBy(() -> new Color(input)) + .isInstanceOf(InvalidColorException.class) + .hasMessage("노선 색은 존재해야 합니다."); + } + + @Test + @DisplayName("색 이름이 최대 길이를 넘으면 예외를 던진다.") + void validateWithLength() { + final String input = "열한글자가넘는색이름입니다색"; + + assertThatThrownBy(() -> new Color(input)) + .isInstanceOf(InvalidColorException.class) + .hasMessage("노선 색은 11글자까지 가능합니다."); + } + + @ParameterizedTest + @DisplayName("색 이름 형식에 맞지 않는다면 예외를 던진다.") + @ValueSource(strings = {"색", "하양", "white색", "123색"}) + void validateWithInvalidColorFormat(final String input) { + assertThatThrownBy(() -> new Color(input)) + .isInstanceOf(InvalidColorException.class) + .hasMessage("색 이름은 한글로 이루어져 있고, '색'으로 끝나야 합니다."); + } +} diff --git a/src/test/java/subway/domain/line/LineTest.java b/src/test/java/subway/domain/line/LineTest.java new file mode 100644 index 000000000..4b959978f --- /dev/null +++ b/src/test/java/subway/domain/line/LineTest.java @@ -0,0 +1,192 @@ +package subway.domain.line; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import subway.domain.section.Section; +import subway.domain.station.Station; +import subway.exception.InvalidDistanceException; +import subway.exception.InvalidSectionException; + +class LineTest { + + private Line line; + private Station upward; + private Station downward; + + @BeforeEach + void setUp() { + upward = new Station(1L, "잠실역"); + downward = new Station(2L, "종합운동장역"); + final List
sections = List.of( + new Section(upward, downward, 10), + new Section(downward, Station.TERMINAL, 0) + ); + line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + } + + @Nested + @DisplayName("노선 역 추가 시 ") + class AddSection { + + @Test + @DisplayName("노선에 역을 최초 추가한다.") + void addFirstSection() { + final Station upward = new Station(1L, "잠실역"); + final Station downward = new Station(2L, "종합운동장역"); + final Line line = new Line(1L, "2호선", "초록색"); + + line.addSection(upward, downward, 10); + + final List result = line.getStations(); + assertAll( + () -> assertThat(result).containsExactly(upward, downward), + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10, 0) + ); + } + + @Test + @DisplayName("중간에 상행역을 추가한다.") + void addUpwardSection() { + final Station additionStation = new Station(3L, "잠실새내역"); + + line.addSection(additionStation, downward, 5); + + final List result = line.getStations(); + assertAll( + () -> assertThat(result).containsExactly(upward, additionStation, downward), + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 5, 0) + ); + } + + @Test + @DisplayName("중간에 하행역을 추가한다.") + void addDownwardSection() { + final Station additionStation = new Station(3L, "잠실새내역"); + + line.addSection(upward, additionStation, 5); + + final List result = line.getStations(); + assertAll( + () -> assertThat(result).containsExactly(upward, additionStation, downward), + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 5, 0) + ); + } + + @Test + @DisplayName("맨 앞에 역을 추가한다.") + void addSectionAtFirst() { + final Station additionStation = new Station(3L, "잠실새내역"); + + line.addSection(additionStation, upward, 5); + + final List result = line.getStations(); + assertAll( + () -> assertThat(result).containsExactly(additionStation, upward, downward), + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 10, 0) + ); + } + + @Test + @DisplayName("맨 뒤에 역을 추가한다.") + void addSectionAtLast() { + final Station additionStation = new Station(3L, "잠실새내역"); + + line.addSection(downward, additionStation, 5); + + final List result = line.getStations(); + assertAll( + () -> assertThat(result).containsExactly(upward, downward, additionStation), + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10, 5, 0) + ); + } + + @Test + @DisplayName("역이 둘다 존재한다면 예외를 던진다.") + void addSectionWithExistStations() { + assertThatThrownBy(() -> line.addSection(upward, downward, 5)) + .isInstanceOf(InvalidSectionException.class) + .hasMessage("두 역이 이미 노선에 존재합니다."); + } + + @Test + @DisplayName("역이 둘다 존재하지 않으면 예외를 던진다.") + void addSectionWithoutExistStations() { + final Station newUpward = new Station(3L, "잠실새내역"); + final Station newDownward = new Station(4L, "사당역"); + + assertThatThrownBy(() -> line.addSection(newUpward, newDownward, 5)) + .isInstanceOf(InvalidSectionException.class) + .hasMessage("연결할 역 정보가 없습니다."); + } + + @Test + @DisplayName("추가될 역의 거리가 추가될 위치의 두 역 사이보다 크거나 같으면 예외를 던진다.") + void addSectionWithInvalidRangeDistance() { + final Station additionStation = new Station(3L, "잠실새내역"); + + assertThatThrownBy(() -> line.addSection(upward, additionStation, 10)) + .isInstanceOf(InvalidDistanceException.class) + .hasMessage("추가될 역의 거리는 추가될 위치의 두 역사이의 거리보다 작아야합니다."); + } + } + + @Nested + @DisplayName("노선에서 역 제거할 시 ") + class DeleteStation { + + @Test + @DisplayName("역이 2개일 때 역을 제거한다.") + void deleteStationAtInitialState() { + line.deleteStation(upward); + + final List result = line.getStations(); + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("역이 2개가 아닐 때 맨 앞의 역을 제거한다.") + void deleteStationAtFirst() { + final Station additionStation = new Station(3L, "잠실새내역"); + line.addSection(upward, additionStation, 3); + + line.deleteStation(upward); + + final List result = line.getStations(); + assertAll( + () -> assertThat(result).containsExactly(additionStation, downward), + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(7, 0) + ); + } + + @Test + @DisplayName("역이 2개가 아닐 때 중간의 역을 제거한다.") + void deleteStationBetweenStations() { + final Station additionStation = new Station(3L, "잠실새내역"); + line.addSection(upward, additionStation, 3); + + line.deleteStation(additionStation); + + final List result = line.getStations(); + assertAll( + () -> assertThat(result).containsExactly(upward, downward), + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10, 0) + ); + } + + @Test + @DisplayName("역이 존재하지 않을 때 예외를 던진다.") + void deleteStationWithNotExistStation() { + assertThatThrownBy(() -> line.deleteStation(new Station(3L, "잠실새내역"))) + .isInstanceOf(InvalidSectionException.class) + .hasMessage("노선에 해당 역이 존재하지 않습니다."); + } + } +} diff --git a/src/test/java/subway/domain/line/NameTest.java b/src/test/java/subway/domain/line/NameTest.java new file mode 100644 index 000000000..9b017ab11 --- /dev/null +++ b/src/test/java/subway/domain/line/NameTest.java @@ -0,0 +1,38 @@ +package subway.domain.line; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import subway.exception.InvalidLineNameException; + +class NameTest { + + @Test + @DisplayName("노선 이름을 정상적으로 생성한다.") + void name() { + assertDoesNotThrow(() -> new Name("5호선")); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("노선이 이름이 존재하지 않으면 예외를 던진다.") + void validateWithBlank(final String input) { + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(InvalidLineNameException.class) + .hasMessage("노선 이름은 존재해야 합니다."); + } + + @ParameterizedTest + @DisplayName("노선의 이름 형식이 맞지 않을 경우 예외를 던진다.") + @ValueSource(strings = {"3", "삼호선", "0호선", "A호선", "12호선"}) + void validateWithInvalidNameFormat(final String input) { + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(InvalidLineNameException.class) + .hasMessage("노선 이름은 1~9호선이어야 합니다."); + } +} diff --git a/src/test/java/subway/domain/section/DistanceTest.java b/src/test/java/subway/domain/section/DistanceTest.java new file mode 100644 index 000000000..07dfeb571 --- /dev/null +++ b/src/test/java/subway/domain/section/DistanceTest.java @@ -0,0 +1,30 @@ +package subway.domain.section; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import subway.exception.InvalidDistanceException; + +class DistanceTest { + + @Test + @DisplayName("거리를 정상적으로 생성한다.") + void distance() { + assertDoesNotThrow(() -> new Distance(10)); + } + + @ParameterizedTest + @DisplayName("거리가 0보다 작을 경우 예외를 던진다.") + @ValueSource(ints = {-1, -3}) + void validateWithInvalidRange(final int input) { + assertThatThrownBy(() -> new Distance(input)) + .isInstanceOf(InvalidDistanceException.class) + .hasMessage("역 사이의 거리는 0이상이어야합니다."); + } +} + + diff --git a/src/test/java/subway/domain/station/NameTest.java b/src/test/java/subway/domain/station/NameTest.java new file mode 100644 index 000000000..5b3c71e99 --- /dev/null +++ b/src/test/java/subway/domain/station/NameTest.java @@ -0,0 +1,56 @@ +package subway.domain.station; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import subway.exception.InvalidStationNameException; + +class NameTest { + + @Test + @DisplayName("역 이름을 정상적으로 생성한다.") + void name() { + assertDoesNotThrow(() -> new Name("을지로3가역")); + } + + @Test + @DisplayName("역 이름 최대 글자를 넘으면 예외를 던진다.") + void validateWithInvalidLength() { + final String input = "열한글자가넘는역이름입니다역"; + + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("역 이름은 11글자까지 가능합니다."); + } + + @Test + @DisplayName("역 이름이 역으로 끝나지 않을 경우 예외를 던진다.") + void validateWithInvalidNameFormat() { + assertThatThrownBy(() -> new Name("선릉")) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("역 이름은 한글, 숫자만 가능하고, '역'으로 끝나야 합니다."); + } + + @ParameterizedTest + @DisplayName("역 이름이 한글, 숫자로 구성되지 않을 경우 예외를 던진다.") + @ValueSource(strings = {"역", "NewYork역", "!!역"}) + void validateWithInvalidNameElement(final String input) { + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("역 이름은 한글, 숫자만 가능하고, '역'으로 끝나야 합니다."); + } + + @ParameterizedTest + @DisplayName("이름이 빈 칸이거나 null 일 때 예외를 던진다.") + @NullAndEmptySource + void validateWithBlankName(final String input) { + assertThatThrownBy(() -> new Name(input)) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("역 이름은 공백일 수 없습니다."); + } +} diff --git a/src/test/java/subway/integration/IntegrationTest.java b/src/test/java/subway/integration/IntegrationTest.java deleted file mode 100644 index c30949402..000000000 --- a/src/test/java/subway/integration/IntegrationTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package subway.integration; - -import io.restassured.RestAssured; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.test.annotation.DirtiesContext; - -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class IntegrationTest { - @LocalServerPort - int port; - - @BeforeEach - public void setUp() { - RestAssured.port = port; - } -} diff --git a/src/test/java/subway/integration/LineIntegrationTest.java b/src/test/java/subway/integration/LineIntegrationTest.java deleted file mode 100644 index ad4170205..000000000 --- a/src/test/java/subway/integration/LineIntegrationTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package subway.integration; - -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.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import subway.dto.LineRequest; -import subway.dto.LineResponse; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("지하철 노선 관련 기능") -public class LineIntegrationTest extends IntegrationTest { - private LineRequest lineRequest1; - private LineRequest lineRequest2; - - @BeforeEach - public void setUp() { - super.setUp(); - - lineRequest1 = new LineRequest("신분당선", "bg-red-600"); - lineRequest2 = new LineRequest("구신분당선", "bg-red-600"); - } - - @DisplayName("지하철 노선을 생성한다.") - @Test - void createLine() { - // when - ExtractableResponse response = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.header("Location")).isNotBlank(); - } - - @DisplayName("기존에 존재하는 지하철 노선 이름으로 지하철 노선을 생성한다.") - @Test - void createLineWithDuplicateName() { - // given - RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // when - ExtractableResponse response = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("지하철 노선 목록을 조회한다.") - @Test - void getLines() { - // given - ExtractableResponse createResponse1 = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - ExtractableResponse createResponse2 = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest2) - .when().post("/lines") - .then().log().all(). - extract(); - - // when - ExtractableResponse response = RestAssured - .given().log().all() - .accept(MediaType.APPLICATION_JSON_VALUE) - .when().get("/lines") - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - List expectedLineIds = Stream.of(createResponse1, createResponse2) - .map(it -> Long.parseLong(it.header("Location").split("/")[2])) - .collect(Collectors.toList()); - List resultLineIds = response.jsonPath().getList(".", LineResponse.class).stream() - .map(LineResponse::getId) - .collect(Collectors.toList()); - assertThat(resultLineIds).containsAll(expectedLineIds); - } - - @DisplayName("지하철 노선을 조회한다.") - @Test - void getLine() { - // given - ExtractableResponse createResponse = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // when - Long lineId = Long.parseLong(createResponse.header("Location").split("/")[2]); - ExtractableResponse response = RestAssured - .given().log().all() - .accept(MediaType.APPLICATION_JSON_VALUE) - .when().get("/lines/{lineId}", lineId) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - LineResponse resultResponse = response.as(LineResponse.class); - assertThat(resultResponse.getId()).isEqualTo(lineId); - } - - @DisplayName("지하철 노선을 수정한다.") - @Test - void updateLine() { - // given - ExtractableResponse createResponse = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // when - Long lineId = Long.parseLong(createResponse.header("Location").split("/")[2]); - ExtractableResponse response = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest2) - .when().put("/lines/{lineId}", lineId) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - } - - @DisplayName("지하철 노선을 제거한다.") - @Test - void deleteLine() { - // given - ExtractableResponse createResponse = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // when - Long lineId = Long.parseLong(createResponse.header("Location").split("/")[2]); - ExtractableResponse response = RestAssured - .given().log().all() - .when().delete("/lines/{lineId}", lineId) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); - } -} diff --git a/src/test/java/subway/integration/StationIntegrationTest.java b/src/test/java/subway/integration/StationIntegrationTest.java deleted file mode 100644 index a97d184a0..000000000 --- a/src/test/java/subway/integration/StationIntegrationTest.java +++ /dev/null @@ -1,197 +0,0 @@ -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.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import subway.dto.StationResponse; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("지하철역 관련 기능") -public class StationIntegrationTest extends IntegrationTest { - @DisplayName("지하철역을 생성한다.") - @Test - void createStation() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - - // when - ExtractableResponse response = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.header("Location")).isNotBlank(); - } - - @DisplayName("기존에 존재하는 지하철역 이름으로 지하철역을 생성한다.") - @Test - void createStationWithDuplicateName() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - ExtractableResponse response = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then() - .log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("지하철역 목록을 조회한다.") - @Test - void getStations() { - /// given - Map params1 = new HashMap<>(); - params1.put("name", "강남역"); - ExtractableResponse createResponse1 = RestAssured.given().log().all() - .body(params1) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - Map params2 = new HashMap<>(); - params2.put("name", "역삼역"); - ExtractableResponse createResponse2 = RestAssured.given().log().all() - .body(params2) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - ExtractableResponse response = RestAssured.given().log().all() - .when() - .get("/stations") - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - List expectedStationIds = Stream.of(createResponse1, createResponse2) - .map(it -> Long.parseLong(it.header("Location").split("/")[2])) - .collect(Collectors.toList()); - List resultStationIds = response.jsonPath().getList(".", StationResponse.class).stream() - .map(StationResponse::getId) - .collect(Collectors.toList()); - assertThat(resultStationIds).containsAll(expectedStationIds); - } - - @DisplayName("지하철역을 조회한다.") - @Test - void getStation() { - /// given - Map params1 = new HashMap<>(); - params1.put("name", "강남역"); - ExtractableResponse createResponse = RestAssured.given().log().all() - .body(params1) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - Long stationId = Long.parseLong(createResponse.header("Location").split("/")[2]); - ExtractableResponse response = RestAssured.given().log().all() - .when() - .get("/stations/{stationId}", stationId) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - StationResponse stationResponse = response.as(StationResponse.class); - assertThat(stationResponse.getId()).isEqualTo(stationId); - } - - @DisplayName("지하철역을 수정한다.") - @Test - void updateStation() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - ExtractableResponse createResponse = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - Map otherParams = new HashMap<>(); - otherParams.put("name", "삼성역"); - String uri = createResponse.header("Location"); - ExtractableResponse response = RestAssured.given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(otherParams) - .when() - .put(uri) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - } - - @DisplayName("지하철역을 제거한다.") - @Test - void deleteStation() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - ExtractableResponse createResponse = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - String uri = createResponse.header("Location"); - ExtractableResponse response = RestAssured.given().log().all() - .when() - .delete(uri) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); - } -} diff --git a/src/test/java/subway/repository/LineRepositoryTest.java b/src/test/java/subway/repository/LineRepositoryTest.java new file mode 100644 index 000000000..a2d666007 --- /dev/null +++ b/src/test/java/subway/repository/LineRepositoryTest.java @@ -0,0 +1,166 @@ +package subway.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import subway.dao.LineDao; +import subway.dao.SectionDao; +import subway.dao.StationDao; +import subway.domain.line.Line; +import subway.domain.section.Section; +import subway.domain.station.Station; +import subway.exception.InvalidLineException; + +@JdbcTest +class LineRepositoryTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + private LineRepository lineRepository; + private StationRepository stationRepository; + + @BeforeEach + void setUp() { + final StationDao stationDao = new StationDao(jdbcTemplate); + final LineDao lineDao = new LineDao(jdbcTemplate); + final SectionDao sectionDao = new SectionDao(jdbcTemplate); + lineRepository = new LineRepository(lineDao, sectionDao); + stationRepository = new StationRepository(stationDao); + } + + @Test + @DisplayName("노선을 저장한다.") + void save() { + final Line line = new Line("2호선", "초록색"); + + final Line result = lineRepository.save(line); + + assertAll( + () -> assertThat(result.getId()).isNotNull(), + () -> assertThat(result.getName()).isEqualTo("2호선"), + () -> assertThat(result.getColor()).isEqualTo("초록색") + ); + } + + @Nested + @DisplayName("노선 조회 시 ") + class FindById { + + private Line line; + + @BeforeEach + void setUp() { + line = lineRepository.save(new Line("2호선", "초록색")); + final Station upward = stationRepository.save(new Station("잠실역")); + final Station downward = stationRepository.save(new Station("잠실새내역")); + line.addSection(upward, downward, 10); + lineRepository.update(line); + } + + @Test + @DisplayName("ID로 조회할 때 존재하는 노선이라면 노선 정보를 반환한다.") + void findById() { + final Line result = lineRepository.findById(line.getId()); + + final List
sections = result.getSections(); + assertAll( + () -> assertThat(result.getId()).isEqualTo(line.getId()), + () -> assertThat(result.getName()).isEqualTo(line.getName()), + () -> assertThat(result.getColor()).isEqualTo(line.getColor()), + () -> assertThat(sections).hasSize(2), + () -> assertThat(sections.get(0).getUpward().getName()).isEqualTo("잠실역"), + () -> assertThat(sections.get(0).getDownward().getName()).isEqualTo("잠실새내역"), + () -> assertThat(sections.get(0).getDistance()).isEqualTo(10), + () -> assertThat(sections.get(1).getUpward().getName()).isEqualTo("잠실새내역"), + () -> assertThat(sections.get(1).getDownward().getName()).isEqualTo(Station.TERMINAL.getName()), + () -> assertThat(sections.get(1).getDistance()).isEqualTo(0) + ); + } + + @Test + @DisplayName("ID로 조회할 때 존재하지 않는 노선이라면 예외를 던진다.") + void findByInvalidId() { + assertThatThrownBy(() -> lineRepository.findById(-2L)) + .isInstanceOf(InvalidLineException.class) + .hasMessage("존재하지 않는 노선 ID 입니다."); + } + + @Test + @DisplayName("모든 노선 정보를 조회한다.") + void findAll() { + final List lines = lineRepository.findAll(); + + final List
sections = lines.get(0).getSections(); + assertAll( + () -> assertThat(lines).hasSize(1), + () -> assertThat(lines.get(0).getName()).isEqualTo("2호선"), + () -> assertThat(lines.get(0).getColor()).isEqualTo("초록색"), + () -> assertThat(sections).hasSize(2), + () -> assertThat(sections.get(0).getUpward().getName()).isEqualTo("잠실역"), + () -> assertThat(sections.get(0).getDownward().getName()).isEqualTo("잠실새내역"), + () -> assertThat(sections.get(0).getDistance()).isEqualTo(10), + () -> assertThat(sections.get(1).getUpward().getName()).isEqualTo("잠실새내역"), + () -> assertThat(sections.get(1).getDownward().getName()).isEqualTo(Station.TERMINAL.getName()), + () -> assertThat(sections.get(1).getDistance()).isEqualTo(0) + ); + } + } + + @Nested + @DisplayName("노선 정보 업데이트 시") + class Update { + + @Test + @DisplayName("섹션이 추가 됐을 때 노선 정보를 업데이트한다.") + void updateWhenStationAdded() { + final Line line = lineRepository.save(new Line("2호선", "초록색")); + final Station upward = stationRepository.save(new Station("잠실역")); + final Station middle = stationRepository.save(new Station("종합운동장역")); + final Station downward = stationRepository.save(new Station("잠실새내역")); + line.addSection(upward, downward, 10); + lineRepository.update(line); + + line.addSection(upward, middle, 3); + lineRepository.update(line); + + final Line result = lineRepository.findById(line.getId()); + final List stations = result.getStations(); + assertAll( + () -> assertThat(stations).hasSize(3), + () -> assertThat(stations).extracting(Station::getName).containsExactly( + "잠실역", + "종합운동장역", + "잠실새내역" + ) + ); + } + + + @Test + @DisplayName("섹션이 삭제 됐을 때 노선 정보를 업데이트한다.") + void updateWhenStationDeleted() { + final Line line = lineRepository.save(new Line("2호선", "초록색")); + final Station upward = stationRepository.save(new Station("잠실역")); + final Station downward = stationRepository.save(new Station("잠실새내역")); + line.addSection(upward, downward, 10); + lineRepository.update(line); + + line.deleteStation(upward); + lineRepository.update(line); + + final Line result = lineRepository.findById(line.getId()); + final List stations = result.getStations(); + assertThat(stations).isEmpty(); + } + } +} diff --git a/src/test/java/subway/repository/StationRepositoryTest.java b/src/test/java/subway/repository/StationRepositoryTest.java new file mode 100644 index 000000000..e0b6b49e1 --- /dev/null +++ b/src/test/java/subway/repository/StationRepositoryTest.java @@ -0,0 +1,70 @@ +package subway.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import subway.dao.StationDao; +import subway.domain.station.Station; +import subway.exception.InvalidStationException; + +@JdbcTest +class StationRepositoryTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + private StationRepository stationRepository; + + @BeforeEach + void setUp() { + final StationDao stationDao = new StationDao(jdbcTemplate); + stationRepository = new StationRepository(stationDao); + } + + @Test + @DisplayName("역을 저장한다.") + void save() { + final Station station = new Station("잠실역"); + + final Station result = stationRepository.save(station); + + assertAll( + () -> assertThat(result.getId()).isNotNull(), + () -> assertThat(result.getName()).isEqualTo(station.getName()) + ); + } + + @Nested + @DisplayName("역을 조회 시 ") + class FindById { + + @Test + @DisplayName("존재하는 역이라면 역 정보를 반환한다.") + void findById() { + final Station station = stationRepository.save(new Station("잠실역")); + + final Station result = stationRepository.findById(station.getId()); + + assertAll( + () -> assertThat(result.getId()).isEqualTo(station.getId()), + () -> assertThat(result.getName()).isEqualTo(station.getName()) + ); + } + + @Test + @DisplayName("존재하지 않는 역이라면 예외를 던진다.") + void findByInvalidId() { + assertThatThrownBy(() -> stationRepository.findById(-2L)) + .isInstanceOf(InvalidStationException.class) + .hasMessage("존재하지 않은 역 ID입니다."); + } + } +} diff --git a/src/test/java/subway/service/LineServiceTest.java b/src/test/java/subway/service/LineServiceTest.java new file mode 100644 index 000000000..013a18b60 --- /dev/null +++ b/src/test/java/subway/service/LineServiceTest.java @@ -0,0 +1,216 @@ +package subway.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import subway.controller.dto.request.LineCreateRequest; +import subway.controller.dto.request.SectionCreateRequest; +import subway.controller.dto.response.LineResponse; +import subway.controller.dto.response.LinesResponse; +import subway.domain.line.Line; +import subway.domain.section.Section; +import subway.domain.station.Station; +import subway.exception.InvalidDistanceException; +import subway.exception.InvalidLineNameException; +import subway.exception.InvalidSectionException; +import subway.repository.LineRepository; +import subway.repository.StationRepository; + +@ExtendWith(MockitoExtension.class) +class LineServiceTest { + + @InjectMocks + private LineService lineService; + + @Mock + private LineRepository lineRepository; + + @Mock + private StationRepository stationRepository; + + @Test + @DisplayName("노선 목록을 조회한다.") + void findLines() { + final List
sectionsOfLineTwo = List.of( + new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), + new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) + ); + final List
sectionsOfLineFour = List.of( + new Section(new Station(3L, "이수역"), new Station(4L, "서울역"), 11), + new Section(new Station(4L, "서울역"), Station.TERMINAL, 0) + ); + final List lines = List.of( + new Line(1L, "2호선", "초록색", sectionsOfLineTwo), + new Line(2L, "4호선", "하늘색", sectionsOfLineFour) + ); + given(lineRepository.findAll()).willReturn(lines); + + final LinesResponse response = lineService.findLines(); + + assertAll( + () -> assertThat(response.getLines()).hasSize(2), + () -> assertThat(response.getLines().get(0).getId()).isEqualTo(1L), + () -> assertThat(response.getLines().get(0).getName()).isEqualTo("2호선"), + () -> assertThat(response.getLines().get(0).getColor()).isEqualTo("초록색"), + () -> assertThat(response.getLines().get(0).getStations()).hasSize(2), + () -> assertThat(response.getLines().get(0).getStations().get(0).getId()).isEqualTo(1L), + () -> assertThat(response.getLines().get(0).getStations().get(0).getName()).isEqualTo("잠실역"), + () -> assertThat(response.getLines().get(0).getStations().get(1).getId()).isEqualTo(2L), + () -> assertThat(response.getLines().get(0).getStations().get(1).getName()).isEqualTo("잠실새내역"), + () -> assertThat(response.getLines().get(1).getId()).isEqualTo(2L), + () -> assertThat(response.getLines().get(1).getName()).isEqualTo("4호선"), + () -> assertThat(response.getLines().get(1).getColor()).isEqualTo("하늘색"), + () -> assertThat(response.getLines().get(1).getStations()).hasSize(2), + () -> assertThat(response.getLines().get(1).getStations().get(0).getId()).isEqualTo(3L), + () -> assertThat(response.getLines().get(1).getStations().get(0).getName()).isEqualTo("이수역"), + () -> assertThat(response.getLines().get(1).getStations().get(1).getId()).isEqualTo(4L), + () -> assertThat(response.getLines().get(1).getStations().get(1).getName()).isEqualTo("서울역") + ); + } + + @Nested + @DisplayName("노선 추가시 ") + class CreateLine { + + @Test + @DisplayName("유효한 정보라면 노선을 추가한다.") + void createLine() { + final Line line = new Line(1L, "2호선", "초록색"); + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색"); + given(lineRepository.save(any(Line.class))).willReturn(line); + + final Long lineId = lineService.createLine(request); + + assertThat(lineId).isEqualTo(1L); + } + + @Test + @DisplayName("유효하지 않은 정보라면 예외를 던진다.") + void createLineWithInvalidName() { + final LineCreateRequest request = new LineCreateRequest("경의중앙선", "초록색"); + + assertThatThrownBy(() -> lineService.createLine(request)) + .isInstanceOf(InvalidLineNameException.class); + } + } + + @Nested + @DisplayName("노선 조회시 ") + class FindLineById { + + @Test + @DisplayName("존재하는 노선이라면 노선 정보를 조회한다.") + void findLineById() { + final List
sections = List.of( + new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), + new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) + ); + final Line line = new Line(1L, "2호선", "초록색", sections); + given(lineRepository.findById(1L)).willReturn(line); + + final LineResponse response = lineService.findLineById(1L); + + assertAll( + () -> assertThat(response.getId()).isEqualTo(1L), + () -> assertThat(response.getName()).isEqualTo("2호선"), + () -> assertThat(response.getColor()).isEqualTo("초록색"), + () -> assertThat(response.getStations()).hasSize(2), + () -> assertThat(response.getStations().get(0).getId()).isEqualTo(1L), + () -> assertThat(response.getStations().get(0).getName()).isEqualTo("잠실역"), + () -> assertThat(response.getStations().get(1).getId()).isEqualTo(2L), + () -> assertThat(response.getStations().get(1).getName()).isEqualTo("잠실새내역") + ); + } + } + + @Nested + @DisplayName("노선에 역 등록 시") + class CreateSection { + + @Test + @DisplayName("유효한 정보라면 노선에 역을 등록한다.") + void createSection() { + final SectionCreateRequest request = new SectionCreateRequest(1L, 3L, 2); + final List
sections = List.of( + new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), + new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) + ); + final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + given(lineRepository.findById(1L)).willReturn(line); + given(stationRepository.findById(1L)).willReturn(new Station(1L, "잠실역")); + given(stationRepository.findById(3L)).willReturn(new Station(3L, "종합운동장역")); + willDoNothing().given(lineRepository).update(any(Line.class)); + + lineService.createSection(1L, request); + + assertThat(line.getSections()).hasSize(3); + } + + @Test + @DisplayName("유효하지 않은 정보라면 예외를 던진다.") + void createSectionWithInvalidDistance() { + final SectionCreateRequest request = new SectionCreateRequest(1L, 3L, 10); + final List
sections = List.of( + new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), + new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) + ); + final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + given(lineRepository.findById(1L)).willReturn(line); + given(stationRepository.findById(1L)).willReturn(new Station(1L, "잠실역")); + given(stationRepository.findById(3L)).willReturn(new Station(3L, "종합운동장역")); + + assertThatThrownBy(() -> lineService.createSection(1L, request)) + .isInstanceOf(InvalidDistanceException.class); + } + } + + @Nested + @DisplayName("노선에서 역 삭제시 ") + class DeleteStation { + + @Test + @DisplayName("유효한 정보라면 역을 삭제한다.") + void deleteStation() { + final List
sections = List.of( + new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), + new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) + ); + final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + given(lineRepository.findById(1L)).willReturn(line); + given(stationRepository.findById(1L)).willReturn(new Station(1L, "잠실역")); + willDoNothing().given(lineRepository).update(any(Line.class)); + + lineService.deleteStation(1L, 1L); + + assertThat(line.getSections()).isEmpty(); + } + + @Test + @DisplayName("유효하지 않은 정보라면 예외를 던진다.") + void deleteStationWithInvalidStationId() { + final List
sections = List.of( + new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), + new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) + ); + final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + given(lineRepository.findById(1L)).willReturn(line); + given(stationRepository.findById(3L)).willReturn(new Station(3L, "종합운동장역")); + + assertThatThrownBy(() -> lineService.deleteStation(1L, 3L)) + .isInstanceOf(InvalidSectionException.class); + } + } +} diff --git a/src/test/java/subway/service/StationServiceTest.java b/src/test/java/subway/service/StationServiceTest.java new file mode 100644 index 000000000..21ca57263 --- /dev/null +++ b/src/test/java/subway/service/StationServiceTest.java @@ -0,0 +1,89 @@ +package subway.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import subway.controller.dto.request.StationCreateRequest; +import subway.controller.dto.response.StationResponse; +import subway.domain.station.Station; +import subway.exception.InvalidStationException; +import subway.exception.InvalidStationNameException; +import subway.repository.StationRepository; + +@ExtendWith(MockitoExtension.class) +class StationServiceTest { + + @InjectMocks + private StationService stationService; + + @Mock + private StationRepository stationRepository; + + @Nested + @DisplayName("역 생성 시 ") + class CreateStation { + + @Test + @DisplayName("정보가 유효하면 역을 생성한다.") + void createStation() { + final StationCreateRequest request = new StationCreateRequest("잠실역"); + final Station station = new Station(1L, "잠실역"); + given(stationRepository.save(any(Station.class))).willReturn(station); + + final Long stationId = stationService.createStation(request); + + assertThat(stationId).isEqualTo(1L); + } + + @Test + @DisplayName("역 이름이 유효하지 않으면 예외를 던진다.") + void createStationWithInvalidName() { + final StationCreateRequest request = new StationCreateRequest("잠실"); + + assertThatThrownBy(() -> stationService.createStation(request)) + .isInstanceOf(InvalidStationNameException.class); + } + } + + @Nested + @DisplayName("역을 조회 시 ") + class FindStationById { + + @Test + @DisplayName("존재하는 역일시 역 정보를 반환한다.") + void findStationById() { + final Station station = new Station(1L, "잠실역"); + given(stationRepository.findById(1L)).willReturn(station); + + final StationResponse result = stationService.findStationById(1L); + + assertAll( + () -> assertThat(result.getId()).isEqualTo(station.getId()), + () -> assertThat(result.getName()).isEqualTo(station.getName()) + ); + } + + @Test + @DisplayName("존재하지 않는 역일시 예외를 던진다.") + void findStationByInvalidId() { + final InvalidStationException exception = new InvalidStationException("존재하지 않는 역입니다."); + willThrow(exception).given(stationRepository).findById(anyLong()); + + assertThatThrownBy(() -> stationService.findStationById(1L)) + .isInstanceOf(InvalidStationException.class) + .hasMessage("존재하지 않는 역입니다."); + } + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 000000000..a07e737ec --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,8 @@ +spring: + h2: + console: + enabled: true + + datasource: + url: jdbc:h2:mem:testdb;MODE=MySQL + driver-class-name: org.h2.Driver diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 000000000..a7a4c1c3f --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS STATION +( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(15) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS LINE +( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(15) NOT NULL, + color VARCHAR(15) NOT NULL, + PRIMARY KEY (id) + ); + +CREATE TABLE IF NOT EXISTS SECTION +( + id BIGINT AUTO_INCREMENT NOT NULL, + line_id BIGINT NOT NULL, + upward_station_id BIGINT NOT NULL, + downward_station_id BIGINT NOT NULL, + distance INT NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (line_id) REFERENCES LINE (id), + FOREIGN KEY (upward_station_id) REFERENCES STATION (id), + FOREIGN KEY (downward_station_id) REFERENCES STATION (id) +); diff --git a/src/test/resources/logback-access.xml b/src/test/resources/logback-access.xml new file mode 100644 index 000000000..38e0823f4 --- /dev/null +++ b/src/test/resources/logback-access.xml @@ -0,0 +1,8 @@ + + + + %n###### HTTP Request ######%n%fullRequest%n###### HTTP Response ######%n%fullResponse%n%n + + + + \ No newline at end of file From d95a57e0511d3d92d51d6edca78608d7afa9b5eb Mon Sep 17 00:00:00 2001 From: woo-chang Date: Mon, 15 May 2023 18:15:38 +0900 Subject: [PATCH 02/40] =?UTF-8?q?build:=20=EC=B5=9C=EB=8B=A8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index ebb673df5..796f38b87 100644 --- a/build.gradle +++ b/build.gradle @@ -1,30 +1,29 @@ plugins { - id 'java' - id 'org.springframework.boot' version '2.7.9' - id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'java' + id 'org.springframework.boot' version '2.7.9' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' } sourceCompatibility = '11' repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - implementation 'org.springframework.boot:spring-boot-starter-validation' - - implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' - + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' + implementation 'org.jgrapht:jgrapht:1.5.2' - testImplementation 'io.rest-assured:rest-assured:4.4.0' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured:4.4.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' - runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.h2database:h2' } test { - useJUnitPlatform() + useJUnitPlatform() } From 28d58425d5d33a094a9a6dba37eba91c99211974 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Mon, 15 May 2023 22:02:07 +0900 Subject: [PATCH 03/40] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 5c77ec0da..04f33ea46 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,16 @@ http://localhost:8080/swagger-ui/index.html#/ -[x] 역을 제거하면 남은 역을 재배치 해야 한다. -[x] 노선에서 역이 제거되면 역과 역 사이의 거리도 재배정되어야 한다. -[x] 노선에 등록된 역이 2개인 경우 하나의 역을 제거할 때 두 역이 모두 제거되어야 한다. + +### 지하철 + +- [ ] 출발역과 도착역 사이의 최단 경로를 구한다. + - [ ] 총 거리 정보를 함께 응답한다. + +### 요금 정책 + +- [ ] 거리에 따른 요금을 계산한다. + - 기본운임(10㎞ 이내): 기본운임 1,250원 + - 이용 거리 초과 시 추가운임 부과 + - 10km~50km: 5km 까지 마다 100원 추가 + - 50km 초과: 8km 까지 마다 100원 추가 From 189f1406a668450c807d1ec963d9975876492345 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Mon, 15 May 2023 22:31:56 +0900 Subject: [PATCH 04/40] =?UTF-8?q?feat:=20=EA=B1=B0=EB=A6=AC=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=9A=94=EA=B8=88=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../subway/domain/billing/BillingPolicy.java | 6 ++ .../domain/billing/DistanceBillingPolicy.java | 27 +++++++++ .../billing/DistanceBillingPolicyTest.java | 59 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/main/java/subway/domain/billing/BillingPolicy.java create mode 100644 src/main/java/subway/domain/billing/DistanceBillingPolicy.java create mode 100644 src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java diff --git a/README.md b/README.md index 04f33ea46..a5357f3f3 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ http://localhost:8080/swagger-ui/index.html#/ ### 요금 정책 -- [ ] 거리에 따른 요금을 계산한다. +- [x] 거리에 따른 요금을 계산한다. - 기본운임(10㎞ 이내): 기본운임 1,250원 - 이용 거리 초과 시 추가운임 부과 - 10km~50km: 5km 까지 마다 100원 추가 diff --git a/src/main/java/subway/domain/billing/BillingPolicy.java b/src/main/java/subway/domain/billing/BillingPolicy.java new file mode 100644 index 000000000..fd79f68f0 --- /dev/null +++ b/src/main/java/subway/domain/billing/BillingPolicy.java @@ -0,0 +1,6 @@ +package subway.domain.billing; + +public interface BillingPolicy { + + int calculateFee(final int distance); +} diff --git a/src/main/java/subway/domain/billing/DistanceBillingPolicy.java b/src/main/java/subway/domain/billing/DistanceBillingPolicy.java new file mode 100644 index 000000000..05b16e243 --- /dev/null +++ b/src/main/java/subway/domain/billing/DistanceBillingPolicy.java @@ -0,0 +1,27 @@ +package subway.domain.billing; + +import subway.exception.InvalidDistanceException; + +public class DistanceBillingPolicy implements BillingPolicy { + + private static final int BASELINE_DISTANCE = 10; + private static final int BASELINE_FEE = 1250; + private static final int EXTRA_CHARGE_DISTANCE = 5; + private static final int EXTRA_CHARGE_FEE = 100; + + @Override + public int calculateFee(final int distance) { + if (distance < 0) { + throw new InvalidDistanceException("거리가 0보다 작은 경우 요금을 계산할 수 없습니다."); + } + if (distance <= BASELINE_DISTANCE) { + return BASELINE_FEE * (int) Math.ceil((double) distance / BASELINE_DISTANCE); + } + final int overDistance = distance - BASELINE_DISTANCE; + return BASELINE_FEE + calculateOverFare(overDistance); + } + + private int calculateOverFare(final int distance) { + return (int) ((Math.ceil((distance - 1) / EXTRA_CHARGE_DISTANCE) + 1) * EXTRA_CHARGE_FEE); + } +} diff --git a/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java b/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java new file mode 100644 index 000000000..ae8db63d6 --- /dev/null +++ b/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java @@ -0,0 +1,59 @@ +package subway.domain.billing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import subway.exception.InvalidDistanceException; + +class DistanceBillingPolicyTest { + + private static final DistanceBillingPolicy billingPolicy = new DistanceBillingPolicy(); + + @Nested + @DisplayName("calculateFee 메서드는 ") + class CalculateFee { + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -1}) + @DisplayName("거리가 0보다 작은 경우 예외를 던진다.") + void calculateFeeWithNegativeDistance(final int distance) { + assertThatThrownBy(() -> billingPolicy.calculateFee(distance)) + .isInstanceOf(InvalidDistanceException.class) + .hasMessage("거리가 0보다 작은 경우 요금을 계산할 수 없습니다."); + } + + @Test + @DisplayName("거리가 0인 경우 요금은 0원이다.") + void calculateFeeWithZeroDistance() { + final int distance = 0; + + final int result = billingPolicy.calculateFee(distance); + + assertThat(result).isEqualTo(0); + } + + @ParameterizedTest + @ValueSource(ints = {1, 5, 10}) + @DisplayName("거리가 기본운임 거리인 10km보다 작거나 같은 경우 요금은 기본요금인 1250원이다.") + void calculateFeeWithBaselineDistance(final int distance) { + final int result = billingPolicy.calculateFee(distance); + + assertThat(result).isEqualTo(1250); + } + + @ParameterizedTest + @CsvSource(value = {"12,1350", "16,1450", "58,2250"}) + @DisplayName("거리가 10km보다 클 때 5km마다 100원이 추가된 요금이 적용된다.") + void calculateFeeWithExtraDistance(final int distance, final int fee) { + final int result = billingPolicy.calculateFee(distance); + + assertThat(result).isEqualTo(fee); + } + } +} From 825bef130c2a1a783990e9fc6649cf21393b569d Mon Sep 17 00:00:00 2001 From: woo-chang Date: Mon, 15 May 2023 23:24:13 +0900 Subject: [PATCH 05/40] =?UTF-8?q?build:=20=EC=B5=9C=EB=8B=A8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 796f38b87..97fb52170 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' - implementation 'org.jgrapht:jgrapht:1.5.2' + implementation 'org.jgrapht:jgrapht-core:1.5.2' testImplementation 'io.rest-assured:rest-assured:4.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' From 2b05c90dc5c87cb6158cb3462de8a2e3040739db Mon Sep 17 00:00:00 2001 From: woo-chang Date: Mon, 15 May 2023 23:25:08 +0900 Subject: [PATCH 06/40] =?UTF-8?q?refactor:=20Line=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EA=B5=AC=EA=B0=84=20=EC=A0=95=EB=B3=B4=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/domain/line/Line.java | 4 +++- .../java/subway/repository/LineRepository.java | 6 ++---- src/test/java/subway/domain/line/LineTest.java | 14 +++++++------- .../java/subway/repository/LineRepositoryTest.java | 14 ++++---------- src/test/java/subway/service/LineServiceTest.java | 2 +- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/main/java/subway/domain/line/Line.java b/src/main/java/subway/domain/line/Line.java index 29b5d3e70..5f99ab24d 100644 --- a/src/main/java/subway/domain/line/Line.java +++ b/src/main/java/subway/domain/line/Line.java @@ -173,6 +173,8 @@ public List getStations() { } public List
getSections() { - return sections.getValue(); + final List
sections = this.sections.getValue(); + sections.removeIf(section -> section.getDownward() == Station.TERMINAL); + return sections; } } diff --git a/src/main/java/subway/repository/LineRepository.java b/src/main/java/subway/repository/LineRepository.java index fa39fcdbd..3f0e1286c 100644 --- a/src/main/java/subway/repository/LineRepository.java +++ b/src/main/java/subway/repository/LineRepository.java @@ -7,7 +7,6 @@ import subway.dao.SectionDao; import subway.domain.line.Line; import subway.domain.section.Section; -import subway.domain.station.Station; import subway.entity.LineEntity; import subway.entity.SectionEntity; import subway.exception.InvalidLineException; @@ -74,9 +73,8 @@ public void update(final Line line) { } private List generateSectionEntities(final Line line) { - final List
sections = line.getSections(); - sections.removeIf(section -> section.getDownward() == Station.TERMINAL); - return sections.stream() + return line.getSections() + .stream() .map(section -> SectionEntity.of(line.getId(), section)) .collect(Collectors.toUnmodifiableList()); } diff --git a/src/test/java/subway/domain/line/LineTest.java b/src/test/java/subway/domain/line/LineTest.java index 4b959978f..b08963b16 100644 --- a/src/test/java/subway/domain/line/LineTest.java +++ b/src/test/java/subway/domain/line/LineTest.java @@ -48,7 +48,7 @@ void addFirstSection() { final List result = line.getStations(); assertAll( () -> assertThat(result).containsExactly(upward, downward), - () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10, 0) + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10) ); } @@ -62,7 +62,7 @@ void addUpwardSection() { final List result = line.getStations(); assertAll( () -> assertThat(result).containsExactly(upward, additionStation, downward), - () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 5, 0) + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 5) ); } @@ -76,7 +76,7 @@ void addDownwardSection() { final List result = line.getStations(); assertAll( () -> assertThat(result).containsExactly(upward, additionStation, downward), - () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 5, 0) + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 5) ); } @@ -90,7 +90,7 @@ void addSectionAtFirst() { final List result = line.getStations(); assertAll( () -> assertThat(result).containsExactly(additionStation, upward, downward), - () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 10, 0) + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(5, 10) ); } @@ -104,7 +104,7 @@ void addSectionAtLast() { final List result = line.getStations(); assertAll( () -> assertThat(result).containsExactly(upward, downward, additionStation), - () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10, 5, 0) + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10, 5) ); } @@ -162,7 +162,7 @@ void deleteStationAtFirst() { final List result = line.getStations(); assertAll( () -> assertThat(result).containsExactly(additionStation, downward), - () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(7, 0) + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(7) ); } @@ -177,7 +177,7 @@ void deleteStationBetweenStations() { final List result = line.getStations(); assertAll( () -> assertThat(result).containsExactly(upward, downward), - () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10, 0) + () -> assertThat(line.getSections()).extracting(Section::getDistance).containsExactly(10) ); } diff --git a/src/test/java/subway/repository/LineRepositoryTest.java b/src/test/java/subway/repository/LineRepositoryTest.java index a2d666007..faed86cf6 100644 --- a/src/test/java/subway/repository/LineRepositoryTest.java +++ b/src/test/java/subway/repository/LineRepositoryTest.java @@ -77,13 +77,10 @@ void findById() { () -> assertThat(result.getId()).isEqualTo(line.getId()), () -> assertThat(result.getName()).isEqualTo(line.getName()), () -> assertThat(result.getColor()).isEqualTo(line.getColor()), - () -> assertThat(sections).hasSize(2), + () -> assertThat(sections).hasSize(1), () -> assertThat(sections.get(0).getUpward().getName()).isEqualTo("잠실역"), () -> assertThat(sections.get(0).getDownward().getName()).isEqualTo("잠실새내역"), - () -> assertThat(sections.get(0).getDistance()).isEqualTo(10), - () -> assertThat(sections.get(1).getUpward().getName()).isEqualTo("잠실새내역"), - () -> assertThat(sections.get(1).getDownward().getName()).isEqualTo(Station.TERMINAL.getName()), - () -> assertThat(sections.get(1).getDistance()).isEqualTo(0) + () -> assertThat(sections.get(0).getDistance()).isEqualTo(10) ); } @@ -105,13 +102,10 @@ void findAll() { () -> assertThat(lines).hasSize(1), () -> assertThat(lines.get(0).getName()).isEqualTo("2호선"), () -> assertThat(lines.get(0).getColor()).isEqualTo("초록색"), - () -> assertThat(sections).hasSize(2), + () -> assertThat(sections).hasSize(1), () -> assertThat(sections.get(0).getUpward().getName()).isEqualTo("잠실역"), () -> assertThat(sections.get(0).getDownward().getName()).isEqualTo("잠실새내역"), - () -> assertThat(sections.get(0).getDistance()).isEqualTo(10), - () -> assertThat(sections.get(1).getUpward().getName()).isEqualTo("잠실새내역"), - () -> assertThat(sections.get(1).getDownward().getName()).isEqualTo(Station.TERMINAL.getName()), - () -> assertThat(sections.get(1).getDistance()).isEqualTo(0) + () -> assertThat(sections.get(0).getDistance()).isEqualTo(10) ); } } diff --git a/src/test/java/subway/service/LineServiceTest.java b/src/test/java/subway/service/LineServiceTest.java index 013a18b60..e88455201 100644 --- a/src/test/java/subway/service/LineServiceTest.java +++ b/src/test/java/subway/service/LineServiceTest.java @@ -156,7 +156,7 @@ void createSection() { lineService.createSection(1L, request); - assertThat(line.getSections()).hasSize(3); + assertThat(line.getSections()).hasSize(2); } @Test From 03a2ad7d458a4fbe099c3acd04f1ace577b2ab50 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Mon, 15 May 2023 23:29:29 +0900 Subject: [PATCH 07/40] =?UTF-8?q?feat:=20=EC=B6=9C=EB=B0=9C=EC=97=AD?= =?UTF-8?q?=EA=B3=BC=20=EB=8F=84=EC=B0=A9=EC=97=AD=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=9D=98=20=EC=B5=9C=EB=8B=A8=20=EA=B2=BD=EB=A1=9C,=20?= =?UTF-8?q?=EA=B1=B0=EB=A6=AC=EB=A5=BC=20=EA=B5=AC=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../java/subway/domain/subway/Subway.java | 60 +++++++++ .../java/subway/domain/subway/SubwayTest.java | 127 ++++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/main/java/subway/domain/subway/Subway.java create mode 100644 src/test/java/subway/domain/subway/SubwayTest.java diff --git a/README.md b/README.md index a5357f3f3..03c81b2a0 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ http://localhost:8080/swagger-ui/index.html#/ ### 지하철 -- [ ] 출발역과 도착역 사이의 최단 경로를 구한다. - - [ ] 총 거리 정보를 함께 응답한다. +- [x] 출발역과 도착역 사이의 최단 경로를 구한다. + - [x] 총 거리 정보를 함께 응답한다. ### 요금 정책 diff --git a/src/main/java/subway/domain/subway/Subway.java b/src/main/java/subway/domain/subway/Subway.java new file mode 100644 index 000000000..6fb70ba84 --- /dev/null +++ b/src/main/java/subway/domain/subway/Subway.java @@ -0,0 +1,60 @@ +package subway.domain.subway; + +import java.util.List; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; +import subway.domain.line.Line; +import subway.domain.section.Section; +import subway.domain.station.Station; +import subway.exception.InvalidStationNameException; + +public class Subway { + + private final DijkstraShortestPath dijkstraShortestPath; + + public Subway(final List lines) { + this.dijkstraShortestPath = new DijkstraShortestPath(generateGraph(lines)); + } + + private WeightedMultigraph generateGraph(final List lines) { + final WeightedMultigraph graph = new WeightedMultigraph(DefaultWeightedEdge.class); + + for (final Line line : lines) { + drawGraph(graph, line); + } + return graph; + } + + private void drawGraph(final WeightedMultigraph graph, final Line line) { + for (final Section section : line.getSections()) { + final Station upward = section.getUpward(); + final Station downward = section.getDownward(); + addVertex(graph, upward.getName()); + addVertex(graph, downward.getName()); + graph.setEdgeWeight(graph.addEdge(upward.getName(), downward.getName()), section.getDistance()); + } + } + + private void addVertex(final WeightedMultigraph graph, final String stationName) { + if (!graph.containsVertex(stationName)) { + graph.addVertex(stationName); + } + } + + public List getShortestPath(final String start, final String end) { + try { + return dijkstraShortestPath.getPath(start, end).getVertexList(); + } catch (IllegalArgumentException e) { + throw new InvalidStationNameException("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + } + + public int getShortestDistance(final String start, final String end) { + try { + return (int) dijkstraShortestPath.getPath(start, end).getWeight(); + } catch (IllegalArgumentException e) { + throw new InvalidStationNameException("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + } +} diff --git a/src/test/java/subway/domain/subway/SubwayTest.java b/src/test/java/subway/domain/subway/SubwayTest.java new file mode 100644 index 000000000..22b319611 --- /dev/null +++ b/src/test/java/subway/domain/subway/SubwayTest.java @@ -0,0 +1,127 @@ +package subway.domain.subway; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import subway.domain.line.Line; +import subway.domain.station.Station; +import subway.exception.InvalidStationNameException; + +class SubwayTest { + + @Nested + @DisplayName("getShortestPath 메서드는 ") + class GetShortestPath { + + @Test + @DisplayName("지하철 상의 최단 경로를 반환한다.") + void getShortestPath() { + final Station gangnam = new Station(1L, "강남역"); + final Station yangjae = new Station(2L, "양재역"); + final Station gyodae = new Station(3L, "교대역"); + final Station nambu = new Station(4L, "남부터미널역"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + final Line lineOfThree = new Line(3L, "3호선", "주황색"); + final Line lineOfNew = new Line(9L, "9호선", "빨간색"); + lineOfTwo.addSection(gyodae, gangnam, 20); + lineOfThree.addSection(gyodae, nambu, 5); + lineOfThree.addSection(nambu, yangjae, 5); + lineOfNew.addSection(gangnam, yangjae, 5); + + final Subway subway = new Subway(List.of(lineOfTwo, lineOfThree, lineOfNew)); + final List result = subway.getShortestPath(gyodae.getName(), gangnam.getName()); + + assertThat(result).containsExactly("교대역", "남부터미널역", "양재역", "강남역"); + } + + @Test + @DisplayName("출발역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + void getShortestPathWithInvalidStartStation() { + final Station gangnam = new Station(1L, "강남역"); + final Station gyodae = new Station(2L, "교대역"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + lineOfTwo.addSection(gyodae, gangnam, 20); + + final Subway subway = new Subway(List.of(lineOfTwo)); + + assertThatThrownBy(() -> subway.getShortestPath("잠실역", gangnam.getName())) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + + @Test + @DisplayName("도착역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + void getShortestPathWithInvalidEndStation() { + final Station gangnam = new Station(1L, "강남역"); + final Station gyodae = new Station(2L, "교대역"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + lineOfTwo.addSection(gyodae, gangnam, 20); + + final Subway subway = new Subway(List.of(lineOfTwo)); + + assertThatThrownBy(() -> subway.getShortestPath(gangnam.getName(), "잠실역")) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + } + + @Nested + @DisplayName("getShortestDistance 메서드는 ") + class GetShortestDistance { + + @Test + @DisplayName("지하철 상의 최단 거리를 반환한다.") + void getShortestDistance() { + final Station gangnam = new Station(1L, "강남역"); + final Station yangjae = new Station(2L, "양재역"); + final Station gyodae = new Station(3L, "교대역"); + final Station nambu = new Station(4L, "남부터미널역"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + final Line lineOfThree = new Line(3L, "3호선", "주황색"); + final Line lineOfNew = new Line(9L, "9호선", "빨간색"); + lineOfTwo.addSection(gyodae, gangnam, 20); + lineOfThree.addSection(gyodae, nambu, 5); + lineOfThree.addSection(nambu, yangjae, 5); + lineOfNew.addSection(gangnam, yangjae, 5); + + final Subway subway = new Subway(List.of(lineOfTwo, lineOfThree, lineOfNew)); + final int result = subway.getShortestDistance(gyodae.getName(), gangnam.getName()); + + assertThat(result).isEqualTo(15); + } + + @Test + @DisplayName("출발역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + void getShortestDistanceWithInvalidStartStation() { + final Station gangnam = new Station(1L, "강남역"); + final Station gyodae = new Station(2L, "교대역"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + lineOfTwo.addSection(gyodae, gangnam, 20); + + final Subway subway = new Subway(List.of(lineOfTwo)); + + assertThatThrownBy(() -> subway.getShortestDistance("잠실역", gangnam.getName())) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + + @Test + @DisplayName("도착역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + void getShortestDistanceWithInvalidEndStation() { + final Station gangnam = new Station(1L, "강남역"); + final Station gyodae = new Station(2L, "교대역"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + lineOfTwo.addSection(gyodae, gangnam, 20); + + final Subway subway = new Subway(List.of(lineOfTwo)); + + assertThatThrownBy(() -> subway.getShortestDistance(gangnam.getName(), "잠실역")) + .isInstanceOf(InvalidStationNameException.class) + .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + } +} From 2bdd66106491aa35e397751eafb527e6e6d10d0c Mon Sep 17 00:00:00 2001 From: woo-chang Date: Tue, 16 May 2023 14:10:59 +0900 Subject: [PATCH 08/40] =?UTF-8?q?refactor:=20=EC=A7=80=ED=95=98=EC=B2=A0?= =?UTF-8?q?=20=EC=97=AD=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/domain/subway/Subway.java | 23 +++----- src/test/java/fixtures/SubwayFixtures.java | 12 ++++ .../java/subway/domain/subway/SubwayTest.java | 59 ++++++++----------- 3 files changed, 45 insertions(+), 49 deletions(-) create mode 100644 src/test/java/fixtures/SubwayFixtures.java diff --git a/src/main/java/subway/domain/subway/Subway.java b/src/main/java/subway/domain/subway/Subway.java index 6fb70ba84..56600507e 100644 --- a/src/main/java/subway/domain/subway/Subway.java +++ b/src/main/java/subway/domain/subway/Subway.java @@ -17,8 +17,9 @@ public Subway(final List lines) { this.dijkstraShortestPath = new DijkstraShortestPath(generateGraph(lines)); } - private WeightedMultigraph generateGraph(final List lines) { - final WeightedMultigraph graph = new WeightedMultigraph(DefaultWeightedEdge.class); + private WeightedMultigraph generateGraph(final List lines) { + final WeightedMultigraph graph = new WeightedMultigraph( + DefaultWeightedEdge.class); for (final Line line : lines) { drawGraph(graph, line); @@ -26,23 +27,17 @@ private WeightedMultigraph generateGraph(final List return graph; } - private void drawGraph(final WeightedMultigraph graph, final Line line) { + private void drawGraph(final WeightedMultigraph graph, final Line line) { for (final Section section : line.getSections()) { final Station upward = section.getUpward(); final Station downward = section.getDownward(); - addVertex(graph, upward.getName()); - addVertex(graph, downward.getName()); - graph.setEdgeWeight(graph.addEdge(upward.getName(), downward.getName()), section.getDistance()); + graph.addVertex(upward); + graph.addVertex(downward); + graph.setEdgeWeight(graph.addEdge(upward, downward), section.getDistance()); } } - private void addVertex(final WeightedMultigraph graph, final String stationName) { - if (!graph.containsVertex(stationName)) { - graph.addVertex(stationName); - } - } - - public List getShortestPath(final String start, final String end) { + public List getShortestPath(final Station start, final Station end) { try { return dijkstraShortestPath.getPath(start, end).getVertexList(); } catch (IllegalArgumentException e) { @@ -50,7 +45,7 @@ public List getShortestPath(final String start, final String end) { } } - public int getShortestDistance(final String start, final String end) { + public int getShortestDistance(final Station start, final Station end) { try { return (int) dijkstraShortestPath.getPath(start, end).getWeight(); } catch (IllegalArgumentException e) { diff --git a/src/test/java/fixtures/SubwayFixtures.java b/src/test/java/fixtures/SubwayFixtures.java new file mode 100644 index 000000000..dc505439e --- /dev/null +++ b/src/test/java/fixtures/SubwayFixtures.java @@ -0,0 +1,12 @@ +package fixtures; + +import subway.domain.station.Station; + +public class SubwayFixtures { + + public static final Station GANGNAM = new Station(1L, "강남역"); + public static final Station YANGJAE = new Station(2L, "양재역"); + public static final Station GYODAE = new Station(3L, "교대역"); + public static final Station NAMBU = new Station(4L, "남부터미널역"); + public static final Station JAMSIL = new Station(5L, "잠실역"); +} diff --git a/src/test/java/subway/domain/subway/SubwayTest.java b/src/test/java/subway/domain/subway/SubwayTest.java index 22b319611..9621c878f 100644 --- a/src/test/java/subway/domain/subway/SubwayTest.java +++ b/src/test/java/subway/domain/subway/SubwayTest.java @@ -1,5 +1,10 @@ package subway.domain.subway; +import static fixtures.SubwayFixtures.GANGNAM; +import static fixtures.SubwayFixtures.GYODAE; +import static fixtures.SubwayFixtures.JAMSIL; +import static fixtures.SubwayFixtures.NAMBU; +import static fixtures.SubwayFixtures.YANGJAE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -20,35 +25,29 @@ class GetShortestPath { @Test @DisplayName("지하철 상의 최단 경로를 반환한다.") void getShortestPath() { - final Station gangnam = new Station(1L, "강남역"); - final Station yangjae = new Station(2L, "양재역"); - final Station gyodae = new Station(3L, "교대역"); - final Station nambu = new Station(4L, "남부터미널역"); final Line lineOfTwo = new Line(2L, "2호선", "초록색"); final Line lineOfThree = new Line(3L, "3호선", "주황색"); final Line lineOfNew = new Line(9L, "9호선", "빨간색"); - lineOfTwo.addSection(gyodae, gangnam, 20); - lineOfThree.addSection(gyodae, nambu, 5); - lineOfThree.addSection(nambu, yangjae, 5); - lineOfNew.addSection(gangnam, yangjae, 5); + lineOfTwo.addSection(GYODAE, GANGNAM, 20); + lineOfThree.addSection(GYODAE, NAMBU, 5); + lineOfThree.addSection(NAMBU, YANGJAE, 5); + lineOfNew.addSection(GANGNAM, YANGJAE, 5); final Subway subway = new Subway(List.of(lineOfTwo, lineOfThree, lineOfNew)); - final List result = subway.getShortestPath(gyodae.getName(), gangnam.getName()); + final List result = subway.getShortestPath(GYODAE, GANGNAM); - assertThat(result).containsExactly("교대역", "남부터미널역", "양재역", "강남역"); + assertThat(result).containsExactly(GYODAE, NAMBU, YANGJAE, GANGNAM); } @Test @DisplayName("출발역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") void getShortestPathWithInvalidStartStation() { - final Station gangnam = new Station(1L, "강남역"); - final Station gyodae = new Station(2L, "교대역"); final Line lineOfTwo = new Line(2L, "2호선", "초록색"); - lineOfTwo.addSection(gyodae, gangnam, 20); + lineOfTwo.addSection(GYODAE, GANGNAM, 20); final Subway subway = new Subway(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestPath("잠실역", gangnam.getName())) + assertThatThrownBy(() -> subway.getShortestPath(JAMSIL, GANGNAM)) .isInstanceOf(InvalidStationNameException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } @@ -56,14 +55,12 @@ void getShortestPathWithInvalidStartStation() { @Test @DisplayName("도착역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") void getShortestPathWithInvalidEndStation() { - final Station gangnam = new Station(1L, "강남역"); - final Station gyodae = new Station(2L, "교대역"); final Line lineOfTwo = new Line(2L, "2호선", "초록색"); - lineOfTwo.addSection(gyodae, gangnam, 20); + lineOfTwo.addSection(GYODAE, GANGNAM, 20); final Subway subway = new Subway(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestPath(gangnam.getName(), "잠실역")) + assertThatThrownBy(() -> subway.getShortestPath(GANGNAM, JAMSIL)) .isInstanceOf(InvalidStationNameException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } @@ -76,20 +73,16 @@ class GetShortestDistance { @Test @DisplayName("지하철 상의 최단 거리를 반환한다.") void getShortestDistance() { - final Station gangnam = new Station(1L, "강남역"); - final Station yangjae = new Station(2L, "양재역"); - final Station gyodae = new Station(3L, "교대역"); - final Station nambu = new Station(4L, "남부터미널역"); final Line lineOfTwo = new Line(2L, "2호선", "초록색"); final Line lineOfThree = new Line(3L, "3호선", "주황색"); final Line lineOfNew = new Line(9L, "9호선", "빨간색"); - lineOfTwo.addSection(gyodae, gangnam, 20); - lineOfThree.addSection(gyodae, nambu, 5); - lineOfThree.addSection(nambu, yangjae, 5); - lineOfNew.addSection(gangnam, yangjae, 5); + lineOfTwo.addSection(GYODAE, GANGNAM, 20); + lineOfThree.addSection(GYODAE, NAMBU, 5); + lineOfThree.addSection(NAMBU, YANGJAE, 5); + lineOfNew.addSection(GANGNAM, YANGJAE, 5); final Subway subway = new Subway(List.of(lineOfTwo, lineOfThree, lineOfNew)); - final int result = subway.getShortestDistance(gyodae.getName(), gangnam.getName()); + final int result = subway.getShortestDistance(GYODAE, GANGNAM); assertThat(result).isEqualTo(15); } @@ -97,14 +90,12 @@ void getShortestDistance() { @Test @DisplayName("출발역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") void getShortestDistanceWithInvalidStartStation() { - final Station gangnam = new Station(1L, "강남역"); - final Station gyodae = new Station(2L, "교대역"); final Line lineOfTwo = new Line(2L, "2호선", "초록색"); - lineOfTwo.addSection(gyodae, gangnam, 20); + lineOfTwo.addSection(GYODAE, GANGNAM, 20); final Subway subway = new Subway(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestDistance("잠실역", gangnam.getName())) + assertThatThrownBy(() -> subway.getShortestDistance(JAMSIL, GANGNAM)) .isInstanceOf(InvalidStationNameException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } @@ -112,14 +103,12 @@ void getShortestDistanceWithInvalidStartStation() { @Test @DisplayName("도착역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") void getShortestDistanceWithInvalidEndStation() { - final Station gangnam = new Station(1L, "강남역"); - final Station gyodae = new Station(2L, "교대역"); final Line lineOfTwo = new Line(2L, "2호선", "초록색"); - lineOfTwo.addSection(gyodae, gangnam, 20); + lineOfTwo.addSection(GYODAE, GANGNAM, 20); final Subway subway = new Subway(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestDistance(gangnam.getName(), "잠실역")) + assertThatThrownBy(() -> subway.getShortestDistance(GANGNAM, JAMSIL)) .isInstanceOf(InvalidStationNameException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } From 7ffb165ee0f5dd79a11c2a7b5a121275396a5564 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Tue, 16 May 2023 17:39:35 +0900 Subject: [PATCH 09/40] =?UTF-8?q?feat:=20=EC=A7=80=ED=95=98=EC=B2=A0=20?= =?UTF-8?q?=EB=85=B8=EC=84=A0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/subway/SubwayGraph.java | 11 ++++ .../domain/subway/SubwayJgraphtGraph.java | 57 +++++++++++++++++++ ...yTest.java => SubwayJgraphtGraphTest.java} | 52 ++++++++--------- 3 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 src/main/java/subway/domain/subway/SubwayGraph.java create mode 100644 src/main/java/subway/domain/subway/SubwayJgraphtGraph.java rename src/test/java/subway/domain/subway/{SubwayTest.java => SubwayJgraphtGraphTest.java} (60%) diff --git a/src/main/java/subway/domain/subway/SubwayGraph.java b/src/main/java/subway/domain/subway/SubwayGraph.java new file mode 100644 index 000000000..3bcacc2bf --- /dev/null +++ b/src/main/java/subway/domain/subway/SubwayGraph.java @@ -0,0 +1,11 @@ +package subway.domain.subway; + +import java.util.List; +import subway.domain.station.Station; + +public interface SubwayGraph { + + List findShortestPath(final Station start, final Station end); + + long calculateShortestDistance(final Station start, final Station end); +} diff --git a/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java b/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java new file mode 100644 index 000000000..048f6135f --- /dev/null +++ b/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java @@ -0,0 +1,57 @@ +package subway.domain.subway; + +import java.util.List; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; +import subway.domain.line.Line; +import subway.domain.section.Section; +import subway.domain.station.Station; +import subway.exception.InvalidStationException; + +public class SubwayJgraphtGraph implements SubwayGraph { + + private final DijkstraShortestPath dijkstraShortestPath; + + public SubwayJgraphtGraph(final List lines) { + this.dijkstraShortestPath = new DijkstraShortestPath(generateGraph(lines)); + } + + private WeightedMultigraph generateGraph(final List lines) { + final WeightedMultigraph graph = + new WeightedMultigraph(DefaultWeightedEdge.class); + + for (final Line line : lines) { + drawGraph(graph, line); + } + return graph; + } + + private void drawGraph(final WeightedMultigraph graph, final Line line) { + for (final Section section : line.getSections()) { + final Station upward = section.getUpward(); + final Station downward = section.getDownward(); + graph.addVertex(upward); + graph.addVertex(downward); + graph.setEdgeWeight(graph.addEdge(upward, downward), section.getDistance()); + } + } + + @Override + public List findShortestPath(final Station start, final Station end) { + try { + return dijkstraShortestPath.getPath(start, end).getVertexList(); + } catch (IllegalArgumentException e) { + throw new InvalidStationException("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + } + + @Override + public long calculateShortestDistance(final Station start, final Station end) { + try { + return (long) dijkstraShortestPath.getPath(start, end).getWeight(); + } catch (IllegalArgumentException e) { + throw new InvalidStationException("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); + } + } +} diff --git a/src/test/java/subway/domain/subway/SubwayTest.java b/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java similarity index 60% rename from src/test/java/subway/domain/subway/SubwayTest.java rename to src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java index 9621c878f..6e26da603 100644 --- a/src/test/java/subway/domain/subway/SubwayTest.java +++ b/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java @@ -14,16 +14,16 @@ import org.junit.jupiter.api.Test; import subway.domain.line.Line; import subway.domain.station.Station; -import subway.exception.InvalidStationNameException; +import subway.exception.InvalidStationException; -class SubwayTest { +class SubwayJgraphtGraphTest { @Nested - @DisplayName("getShortestPath 메서드는 ") + @DisplayName("findShortestPath 메서드는 ") class GetShortestPath { @Test - @DisplayName("지하철 상의 최단 경로를 반환한다.") + @DisplayName("호선 정보를 통해 최단 경로를 반환한다.") void getShortestPath() { final Line lineOfTwo = new Line(2L, "2호선", "초록색"); final Line lineOfThree = new Line(3L, "3호선", "주황색"); @@ -33,45 +33,45 @@ void getShortestPath() { lineOfThree.addSection(NAMBU, YANGJAE, 5); lineOfNew.addSection(GANGNAM, YANGJAE, 5); - final Subway subway = new Subway(List.of(lineOfTwo, lineOfThree, lineOfNew)); - final List result = subway.getShortestPath(GYODAE, GANGNAM); + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo, lineOfThree, lineOfNew)); + final List result = subwayGraph.findShortestPath(GYODAE, GANGNAM); assertThat(result).containsExactly(GYODAE, NAMBU, YANGJAE, GANGNAM); } @Test - @DisplayName("출발역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + @DisplayName("출발역이 등록되어 있지 않은 경우 예외를 던진다.") void getShortestPathWithInvalidStartStation() { final Line lineOfTwo = new Line(2L, "2호선", "초록색"); lineOfTwo.addSection(GYODAE, GANGNAM, 20); - final Subway subway = new Subway(List.of(lineOfTwo)); + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestPath(JAMSIL, GANGNAM)) - .isInstanceOf(InvalidStationNameException.class) + assertThatThrownBy(() -> subwayGraph.findShortestPath(JAMSIL, GANGNAM)) + .isInstanceOf(InvalidStationException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } @Test - @DisplayName("도착역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + @DisplayName("도착역이 등록되어 있지 않은 경우 예외를 던진다.") void getShortestPathWithInvalidEndStation() { final Line lineOfTwo = new Line(2L, "2호선", "초록색"); lineOfTwo.addSection(GYODAE, GANGNAM, 20); - final Subway subway = new Subway(List.of(lineOfTwo)); + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestPath(GANGNAM, JAMSIL)) - .isInstanceOf(InvalidStationNameException.class) + assertThatThrownBy(() -> subwayGraph.findShortestPath(GANGNAM, JAMSIL)) + .isInstanceOf(InvalidStationException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } } @Nested - @DisplayName("getShortestDistance 메서드는 ") + @DisplayName("calculateShortestDistance 메서드는 ") class GetShortestDistance { @Test - @DisplayName("지하철 상의 최단 거리를 반환한다.") + @DisplayName("호선 정보를 통해 최단 거리를 반환한다.") void getShortestDistance() { final Line lineOfTwo = new Line(2L, "2호선", "초록색"); final Line lineOfThree = new Line(3L, "3호선", "주황색"); @@ -81,35 +81,35 @@ void getShortestDistance() { lineOfThree.addSection(NAMBU, YANGJAE, 5); lineOfNew.addSection(GANGNAM, YANGJAE, 5); - final Subway subway = new Subway(List.of(lineOfTwo, lineOfThree, lineOfNew)); - final int result = subway.getShortestDistance(GYODAE, GANGNAM); + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo, lineOfThree, lineOfNew)); + final long result = subwayGraph.calculateShortestDistance(GYODAE, GANGNAM); assertThat(result).isEqualTo(15); } @Test - @DisplayName("출발역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + @DisplayName("출발역이 등록되어 있지 않은 경우 예외를 던진다.") void getShortestDistanceWithInvalidStartStation() { final Line lineOfTwo = new Line(2L, "2호선", "초록색"); lineOfTwo.addSection(GYODAE, GANGNAM, 20); - final Subway subway = new Subway(List.of(lineOfTwo)); + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestDistance(JAMSIL, GANGNAM)) - .isInstanceOf(InvalidStationNameException.class) + assertThatThrownBy(() -> subwayGraph.calculateShortestDistance(JAMSIL, GANGNAM)) + .isInstanceOf(InvalidStationException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } @Test - @DisplayName("도착역이 지하철에 등록되어 있지 않은 경우 예외를 던진다.") + @DisplayName("도착역이 등록되어 있지 않은 경우 예외를 던진다.") void getShortestDistanceWithInvalidEndStation() { final Line lineOfTwo = new Line(2L, "2호선", "초록색"); lineOfTwo.addSection(GYODAE, GANGNAM, 20); - final Subway subway = new Subway(List.of(lineOfTwo)); + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); - assertThatThrownBy(() -> subway.getShortestDistance(GANGNAM, JAMSIL)) - .isInstanceOf(InvalidStationNameException.class) + assertThatThrownBy(() -> subwayGraph.calculateShortestDistance(GANGNAM, JAMSIL)) + .isInstanceOf(InvalidStationException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } } From 6a4adfa39fa65438b869dce316aa74d766128eab Mon Sep 17 00:00:00 2001 From: woo-chang Date: Tue, 16 May 2023 17:39:52 +0900 Subject: [PATCH 10/40] =?UTF-8?q?chore:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/domain/billing/BillingPolicy.java | 2 +- .../java/subway/domain/billing/DistanceBillingPolicy.java | 2 +- .../subway/domain/billing/DistanceBillingPolicyTest.java | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/subway/domain/billing/BillingPolicy.java b/src/main/java/subway/domain/billing/BillingPolicy.java index fd79f68f0..242444d1c 100644 --- a/src/main/java/subway/domain/billing/BillingPolicy.java +++ b/src/main/java/subway/domain/billing/BillingPolicy.java @@ -2,5 +2,5 @@ public interface BillingPolicy { - int calculateFee(final int distance); + int calculateFare(final int distance); } diff --git a/src/main/java/subway/domain/billing/DistanceBillingPolicy.java b/src/main/java/subway/domain/billing/DistanceBillingPolicy.java index 05b16e243..d18b8690c 100644 --- a/src/main/java/subway/domain/billing/DistanceBillingPolicy.java +++ b/src/main/java/subway/domain/billing/DistanceBillingPolicy.java @@ -10,7 +10,7 @@ public class DistanceBillingPolicy implements BillingPolicy { private static final int EXTRA_CHARGE_FEE = 100; @Override - public int calculateFee(final int distance) { + public int calculateFare(final int distance) { if (distance < 0) { throw new InvalidDistanceException("거리가 0보다 작은 경우 요금을 계산할 수 없습니다."); } diff --git a/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java b/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java index ae8db63d6..1d709d49d 100644 --- a/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java +++ b/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java @@ -23,7 +23,7 @@ class CalculateFee { @ValueSource(ints = {Integer.MIN_VALUE, -1}) @DisplayName("거리가 0보다 작은 경우 예외를 던진다.") void calculateFeeWithNegativeDistance(final int distance) { - assertThatThrownBy(() -> billingPolicy.calculateFee(distance)) + assertThatThrownBy(() -> billingPolicy.calculateFare(distance)) .isInstanceOf(InvalidDistanceException.class) .hasMessage("거리가 0보다 작은 경우 요금을 계산할 수 없습니다."); } @@ -33,7 +33,7 @@ void calculateFeeWithNegativeDistance(final int distance) { void calculateFeeWithZeroDistance() { final int distance = 0; - final int result = billingPolicy.calculateFee(distance); + final int result = billingPolicy.calculateFare(distance); assertThat(result).isEqualTo(0); } @@ -42,7 +42,7 @@ void calculateFeeWithZeroDistance() { @ValueSource(ints = {1, 5, 10}) @DisplayName("거리가 기본운임 거리인 10km보다 작거나 같은 경우 요금은 기본요금인 1250원이다.") void calculateFeeWithBaselineDistance(final int distance) { - final int result = billingPolicy.calculateFee(distance); + final int result = billingPolicy.calculateFare(distance); assertThat(result).isEqualTo(1250); } @@ -51,7 +51,7 @@ void calculateFeeWithBaselineDistance(final int distance) { @CsvSource(value = {"12,1350", "16,1450", "58,2250"}) @DisplayName("거리가 10km보다 클 때 5km마다 100원이 추가된 요금이 적용된다.") void calculateFeeWithExtraDistance(final int distance, final int fee) { - final int result = billingPolicy.calculateFee(distance); + final int result = billingPolicy.calculateFare(distance); assertThat(result).isEqualTo(fee); } From 085ad8f56f6ab554cf016644421a75e2ebd88976 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Tue, 16 May 2023 17:40:37 +0900 Subject: [PATCH 11/40] =?UTF-8?q?feat:=20=EC=A7=80=ED=95=98=EC=B2=A0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B5=9C=EB=8B=A8=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/domain/subway/Path.java | 23 ++++++++ .../java/subway/domain/subway/Subway.java | 52 +++---------------- .../java/subway/domain/subway/SubwayTest.java | 37 +++++++++++++ 3 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 src/main/java/subway/domain/subway/Path.java create mode 100644 src/test/java/subway/domain/subway/SubwayTest.java diff --git a/src/main/java/subway/domain/subway/Path.java b/src/main/java/subway/domain/subway/Path.java new file mode 100644 index 000000000..313a1965e --- /dev/null +++ b/src/main/java/subway/domain/subway/Path.java @@ -0,0 +1,23 @@ +package subway.domain.subway; + +import java.util.List; +import subway.domain.station.Station; + +public class Path { + + private final long distance; + private List stations; + + public Path(final long distance, final List stations) { + this.distance = distance; + this.stations = stations; + } + + public long getDistance() { + return distance; + } + + public List getStations() { + return stations; + } +} diff --git a/src/main/java/subway/domain/subway/Subway.java b/src/main/java/subway/domain/subway/Subway.java index 56600507e..4e9bd6daf 100644 --- a/src/main/java/subway/domain/subway/Subway.java +++ b/src/main/java/subway/domain/subway/Subway.java @@ -1,55 +1,19 @@ package subway.domain.subway; -import java.util.List; -import org.jgrapht.alg.shortestpath.DijkstraShortestPath; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.jgrapht.graph.WeightedMultigraph; -import subway.domain.line.Line; -import subway.domain.section.Section; import subway.domain.station.Station; -import subway.exception.InvalidStationNameException; public class Subway { - private final DijkstraShortestPath dijkstraShortestPath; + private final SubwayGraph subwayGraph; - public Subway(final List lines) { - this.dijkstraShortestPath = new DijkstraShortestPath(generateGraph(lines)); + public Subway(final SubwayGraph subwayGraph) { + this.subwayGraph = subwayGraph; } - private WeightedMultigraph generateGraph(final List lines) { - final WeightedMultigraph graph = new WeightedMultigraph( - DefaultWeightedEdge.class); - - for (final Line line : lines) { - drawGraph(graph, line); - } - return graph; - } - - private void drawGraph(final WeightedMultigraph graph, final Line line) { - for (final Section section : line.getSections()) { - final Station upward = section.getUpward(); - final Station downward = section.getDownward(); - graph.addVertex(upward); - graph.addVertex(downward); - graph.setEdgeWeight(graph.addEdge(upward, downward), section.getDistance()); - } - } - - public List getShortestPath(final Station start, final Station end) { - try { - return dijkstraShortestPath.getPath(start, end).getVertexList(); - } catch (IllegalArgumentException e) { - throw new InvalidStationNameException("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); - } - } - - public int getShortestDistance(final Station start, final Station end) { - try { - return (int) dijkstraShortestPath.getPath(start, end).getWeight(); - } catch (IllegalArgumentException e) { - throw new InvalidStationNameException("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); - } + public Path findShortestPath(final Station start, final Station end) { + return new Path( + subwayGraph.calculateShortestDistance(start, end), + subwayGraph.findShortestPath(start, end) + ); } } diff --git a/src/test/java/subway/domain/subway/SubwayTest.java b/src/test/java/subway/domain/subway/SubwayTest.java new file mode 100644 index 000000000..f339446f2 --- /dev/null +++ b/src/test/java/subway/domain/subway/SubwayTest.java @@ -0,0 +1,37 @@ +package subway.domain.subway; + +import static fixtures.SubwayFixtures.GANGNAM; +import static fixtures.SubwayFixtures.GYODAE; +import static fixtures.SubwayFixtures.NAMBU; +import static fixtures.SubwayFixtures.YANGJAE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import subway.domain.line.Line; + +class SubwayTest { + + @Test + @DisplayName("최단 경로와 최단 거리를 확인한다.") + void findShortestPat() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + final Line lineOfThree = new Line(3L, "3호선", "주황색"); + final Line lineOfNew = new Line(9L, "9호선", "빨간색"); + lineOfTwo.addSection(GYODAE, GANGNAM, 20); + lineOfThree.addSection(GYODAE, NAMBU, 5); + lineOfThree.addSection(NAMBU, YANGJAE, 5); + lineOfNew.addSection(GANGNAM, YANGJAE, 5); + + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo, lineOfThree, lineOfNew)); + final Subway subway = new Subway(subwayGraph); + final Path result = subway.findShortestPath(GYODAE, GANGNAM); + + assertAll( + () -> assertThat(result.getStations()).containsExactly(GYODAE, NAMBU, YANGJAE, GANGNAM), + () -> assertThat(result.getDistance()).isEqualTo(15) + ); + } +} From d3563ffb801a57c33f8ac1bf4d9ba9dc238ad56b Mon Sep 17 00:00:00 2001 From: woo-chang Date: Tue, 16 May 2023 22:26:58 +0900 Subject: [PATCH 12/40] =?UTF-8?q?feat:=20=EC=B5=9C=EB=8B=A8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/controller/SubwayController.java | 28 ++++++ .../dto/request/ShortestPathFindRequest.java | 28 ++++++ .../dto/response/ShortestPathResponse.java | 28 ++++++ ...bwayFixtures.java => StationFixtures.java} | 2 +- .../controller/SubwayControllerTest.java | 98 +++++++++++++++++++ .../domain/subway/SubwayJgraphtGraphTest.java | 10 +- .../java/subway/domain/subway/SubwayTest.java | 8 +- 7 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 src/main/java/subway/controller/SubwayController.java create mode 100644 src/main/java/subway/controller/dto/request/ShortestPathFindRequest.java create mode 100644 src/main/java/subway/controller/dto/response/ShortestPathResponse.java rename src/test/java/fixtures/{SubwayFixtures.java => StationFixtures.java} (93%) create mode 100644 src/test/java/subway/controller/SubwayControllerTest.java diff --git a/src/main/java/subway/controller/SubwayController.java b/src/main/java/subway/controller/SubwayController.java new file mode 100644 index 000000000..23cc33005 --- /dev/null +++ b/src/main/java/subway/controller/SubwayController.java @@ -0,0 +1,28 @@ +package subway.controller; + +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import subway.controller.dto.request.ShortestPathFindRequest; +import subway.controller.dto.response.ShortestPathResponse; +import subway.service.SubwayService; + +@RequestMapping("/subways") +@RestController +public class SubwayController { + + private final SubwayService subwayService; + + public SubwayController(final SubwayService subwayService) { + this.subwayService = subwayService; + } + + @GetMapping("/shortest-path") + public ResponseEntity findShortestPath(@Valid @RequestBody ShortestPathFindRequest request) { + final ShortestPathResponse response = subwayService.findShortestPath(request); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/subway/controller/dto/request/ShortestPathFindRequest.java b/src/main/java/subway/controller/dto/request/ShortestPathFindRequest.java new file mode 100644 index 000000000..e3b80015e --- /dev/null +++ b/src/main/java/subway/controller/dto/request/ShortestPathFindRequest.java @@ -0,0 +1,28 @@ +package subway.controller.dto.request; + +import javax.validation.constraints.NotNull; + +public class ShortestPathFindRequest { + + @NotNull(message = "출발역 ID는 존재해야 합니다.") + private Long startStationId; + + @NotNull(message = "도착역 ID는 존재해야 합니다.") + private Long endStationId; + + private ShortestPathFindRequest() { + } + + public ShortestPathFindRequest(final Long startStationId, final Long endStationId) { + this.startStationId = startStationId; + this.endStationId = endStationId; + } + + public Long getStartStationId() { + return startStationId; + } + + public Long getEndStationId() { + return endStationId; + } +} diff --git a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java new file mode 100644 index 000000000..abd67c43e --- /dev/null +++ b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java @@ -0,0 +1,28 @@ +package subway.controller.dto.response; + +import java.util.List; + +public class ShortestPathResponse { + + private List path; + private long distance; + private long fare; + + public ShortestPathResponse(final List path, final long distance, final long fare) { + this.path = path; + this.distance = distance; + this.fare = fare; + } + + public List getPath() { + return path; + } + + public long getDistance() { + return distance; + } + + public long getFare() { + return fare; + } +} diff --git a/src/test/java/fixtures/SubwayFixtures.java b/src/test/java/fixtures/StationFixtures.java similarity index 93% rename from src/test/java/fixtures/SubwayFixtures.java rename to src/test/java/fixtures/StationFixtures.java index dc505439e..7722fe643 100644 --- a/src/test/java/fixtures/SubwayFixtures.java +++ b/src/test/java/fixtures/StationFixtures.java @@ -2,7 +2,7 @@ import subway.domain.station.Station; -public class SubwayFixtures { +public class StationFixtures { public static final Station GANGNAM = new Station(1L, "강남역"); public static final Station YANGJAE = new Station(2L, "양재역"); diff --git a/src/test/java/subway/controller/SubwayControllerTest.java b/src/test/java/subway/controller/SubwayControllerTest.java new file mode 100644 index 000000000..10dc2daeb --- /dev/null +++ b/src/test/java/subway/controller/SubwayControllerTest.java @@ -0,0 +1,98 @@ +package subway.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import fixtures.StationFixtures; +import java.nio.charset.Charset; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import subway.controller.dto.request.ShortestPathFindRequest; +import subway.controller.dto.response.ShortestPathResponse; +import subway.controller.dto.response.StationResponse; +import subway.service.SubwayService; + +@WebMvcTest(SubwayController.class) +class SubwayControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private SubwayService subwayService; + + @Nested + @DisplayName("findShortestPath 메서드는 ") + class FindShortestPath { + + @Test + @DisplayName("유효한 요청이라면 최단 경로 정보를 반환한다.") + void findShortestPath() throws Exception { + final ShortestPathFindRequest request = new ShortestPathFindRequest(1L, 2L); + final ShortestPathResponse response = new ShortestPathResponse( + List.of( + StationResponse.from(StationFixtures.GANGNAM), + StationResponse.from(StationFixtures.YANGJAE) + ), + 10, + 1250 + ); + + given(subwayService.findShortestPath(any(ShortestPathFindRequest.class))).willReturn(response); + + final MvcResult mvcResult = mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + final String jsonResponse = mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")); + final ShortestPathResponse result = objectMapper.readValue(jsonResponse, ShortestPathResponse.class); + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("출발역 ID가 존재하지 않으면 400 상태를 반환한다.") + void findShortestPathWithInvalidStartStation() throws Exception { + final ShortestPathFindRequest request = new ShortestPathFindRequest(null, 2L); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("출발역 ID는 존재해야 합니다.")); + } + + @Test + @DisplayName("도착역 ID가 존재하지 않으면 400 상태를 반환한다.") + void findShortestPathWithInvalidEndStation() throws Exception { + final ShortestPathFindRequest request = new ShortestPathFindRequest(1L, null); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("도착역 ID는 존재해야 합니다.")); + } + } +} diff --git a/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java b/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java index 6e26da603..ba3b198cf 100644 --- a/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java +++ b/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java @@ -1,10 +1,10 @@ package subway.domain.subway; -import static fixtures.SubwayFixtures.GANGNAM; -import static fixtures.SubwayFixtures.GYODAE; -import static fixtures.SubwayFixtures.JAMSIL; -import static fixtures.SubwayFixtures.NAMBU; -import static fixtures.SubwayFixtures.YANGJAE; +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.GYODAE; +import static fixtures.StationFixtures.JAMSIL; +import static fixtures.StationFixtures.NAMBU; +import static fixtures.StationFixtures.YANGJAE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/src/test/java/subway/domain/subway/SubwayTest.java b/src/test/java/subway/domain/subway/SubwayTest.java index f339446f2..46318b128 100644 --- a/src/test/java/subway/domain/subway/SubwayTest.java +++ b/src/test/java/subway/domain/subway/SubwayTest.java @@ -1,9 +1,9 @@ package subway.domain.subway; -import static fixtures.SubwayFixtures.GANGNAM; -import static fixtures.SubwayFixtures.GYODAE; -import static fixtures.SubwayFixtures.NAMBU; -import static fixtures.SubwayFixtures.YANGJAE; +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.GYODAE; +import static fixtures.StationFixtures.NAMBU; +import static fixtures.StationFixtures.YANGJAE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; From 9de64b813389a50c893cd6414ae87ab5580ffa1b Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 15:26:23 +0900 Subject: [PATCH 13/40] =?UTF-8?q?feat:=20=EB=85=B8=EC=84=A0=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=9A=94=EA=B8=88=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/LineCreateRequest.java | 14 ++- .../controller/dto/response/LineResponse.java | 24 +++++- .../dto/response/LinesResponse.java | 3 + src/main/java/subway/dao/LineDao.java | 18 ++-- src/main/java/subway/domain/line/Line.java | 16 ++-- src/main/java/subway/entity/LineEntity.java | 14 ++- .../subway/repository/LineRepository.java | 9 +- src/main/java/subway/service/LineService.java | 2 +- src/main/resources/data.sql | 3 +- src/main/resources/dummy.sql | 2 +- .../LineControllerIntegrationTest.java | 86 +++++++++++-------- .../subway/controller/LineControllerTest.java | 86 +++++++++++-------- src/test/java/subway/dao/LineDaoTest.java | 32 +++---- src/test/java/subway/dao/SectionDaoTest.java | 23 +---- src/test/java/subway/dao/StationDaoTest.java | 6 +- .../java/subway/domain/line/LineTest.java | 4 +- .../subway/repository/LineRepositoryTest.java | 46 +++------- .../repository/StationRepositoryTest.java | 5 +- .../java/subway/service/LineServiceTest.java | 56 ++++-------- .../subway/service/StationServiceTest.java | 6 +- src/test/resources/data.sql | 3 +- 21 files changed, 229 insertions(+), 229 deletions(-) diff --git a/src/main/java/subway/controller/dto/request/LineCreateRequest.java b/src/main/java/subway/controller/dto/request/LineCreateRequest.java index f79e16c37..d4813f649 100644 --- a/src/main/java/subway/controller/dto/request/LineCreateRequest.java +++ b/src/main/java/subway/controller/dto/request/LineCreateRequest.java @@ -1,7 +1,9 @@ package subway.controller.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; public class LineCreateRequest { @@ -13,12 +15,18 @@ public class LineCreateRequest { @NotBlank(message = "노선 색깔은 공백일 수 없습니다.") private String color; + @Schema(description = "노선 추가 요금") + @NotNull(message = "노선 추가 요금은 존재해야 합니다.") + @Min(value = 0, message = "노선 추가 요금은 0보다 커야합니다.") + private Integer fare; + private LineCreateRequest() { } - public LineCreateRequest(final String name, final String color) { + public LineCreateRequest(final String name, final String color, final Integer fare) { this.name = name; this.color = color; + this.fare = fare; } public String getName() { @@ -28,4 +36,8 @@ public String getName() { public String getColor() { return color; } + + public Integer getFare() { + return fare; + } } diff --git a/src/main/java/subway/controller/dto/response/LineResponse.java b/src/main/java/subway/controller/dto/response/LineResponse.java index 857205826..fe5dd23af 100644 --- a/src/main/java/subway/controller/dto/response/LineResponse.java +++ b/src/main/java/subway/controller/dto/response/LineResponse.java @@ -17,18 +17,34 @@ public class LineResponse { @Schema(description = "노선 색") private String color; + @Schema(description = "노선 추가 요금") + private Integer fare; + @Schema(description = "노선의 역 목록") private List stations; - public LineResponse(final Long id, final String name, final String color, final List stations) { + public LineResponse( + final Long id, + final String name, + final String color, + final Integer fare, + final List stations + ) { this.id = id; this.name = name; this.color = color; + this.fare = fare; this.stations = stations; } public static LineResponse from(final Line line) { - return new LineResponse(line.getId(), line.getName(), line.getColor(), generateStations(line.getStations())); + return new LineResponse( + line.getId(), + line.getName(), + line.getColor(), + line.getFare(), + generateStations(line.getStations()) + ); } private static List generateStations(final List stations) { @@ -49,6 +65,10 @@ public String getColor() { return color; } + public Integer getFare() { + return fare; + } + public List getStations() { return stations; } diff --git a/src/main/java/subway/controller/dto/response/LinesResponse.java b/src/main/java/subway/controller/dto/response/LinesResponse.java index 9a2876933..1ad249d82 100644 --- a/src/main/java/subway/controller/dto/response/LinesResponse.java +++ b/src/main/java/subway/controller/dto/response/LinesResponse.java @@ -8,6 +8,9 @@ public class LinesResponse { @Schema(description = "노선 목록") private List lines; + public LinesResponse() { + } + public LinesResponse(final List lines) { this.lines = lines; } diff --git a/src/main/java/subway/dao/LineDao.java b/src/main/java/subway/dao/LineDao.java index 1bd20ff55..cf99f4a28 100644 --- a/src/main/java/subway/dao/LineDao.java +++ b/src/main/java/subway/dao/LineDao.java @@ -17,7 +17,8 @@ public class LineDao { private static final RowMapper ROW_MAPPER = (rs, rowNum) -> new LineEntity( rs.getLong("id"), rs.getString("name"), - rs.getString("color") + rs.getString("color"), + rs.getInt("fare") ); private final JdbcTemplate jdbcTemplate; @@ -27,18 +28,18 @@ public LineDao(final JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; this.jdbcInsert = new SimpleJdbcInsert(jdbcTemplate) .withTableName("line") - .usingColumns("name", "color") + .usingColumns("name", "color", "fare") .usingGeneratedKeyColumns("id"); } public LineEntity save(final LineEntity lineEntity) { final SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(lineEntity); final Long lineId = jdbcInsert.executeAndReturnKey(parameterSource).longValue(); - return new LineEntity(lineId, lineEntity.getName(), lineEntity.getColor()); + return new LineEntity(lineId, lineEntity.getName(), lineEntity.getColor(), lineEntity.getFare()); } public Optional findById(final Long lineId) { - final String sql = "SELECT id, name, color FROM line WHERE id = ?"; + final String sql = "SELECT id, name, color, fare FROM line WHERE id = ?"; try { final LineEntity result = jdbcTemplate.queryForObject( sql, @@ -52,12 +53,15 @@ public Optional findById(final Long lineId) { } public List findAll() { - final String sql = "SELECT id, name, color FROM line"; + final String sql = "SELECT id, name, color, fare FROM line"; return jdbcTemplate.query(sql, ROW_MAPPER); } public int update(final LineEntity lineEntity) { - final String sql = "UPDATE line SET name = ?, color = ? WHERE id = ?"; - return jdbcTemplate.update(sql, lineEntity.getName(), lineEntity.getColor(), lineEntity.getId()); + final String sql = "UPDATE line SET name = ?, color = ?, fare = ? WHERE id = ?"; + return jdbcTemplate.update( + sql, + lineEntity.getName(), lineEntity.getColor(), lineEntity.getFare(), lineEntity.getId() + ); } } diff --git a/src/main/java/subway/domain/line/Line.java b/src/main/java/subway/domain/line/Line.java index 5f99ab24d..d768479e3 100644 --- a/src/main/java/subway/domain/line/Line.java +++ b/src/main/java/subway/domain/line/Line.java @@ -16,20 +16,22 @@ public final class Line { private final Long id; private final Name name; private final Color color; + private final Fare fare; private final Sections sections; - public Line(final String name, final String color) { - this(null, name, color); + public Line(final String name, final String color, final int fare) { + this(null, name, color, fare); } - public Line(final Long id, final String name, final String color) { - this(id, name, color, new LinkedList<>()); + public Line(final Long id, final String name, final String color, final int fare) { + this(id, name, color, fare, new LinkedList<>()); } - public Line(final Long id, final String name, final String color, final List
sections) { + public Line(final Long id, final String name, final String color, final int fare, final List
sections) { this.id = id; this.name = new Name(name); this.color = new Color(color); + this.fare = new Fare(fare); this.sections = new Sections(sections); } @@ -168,6 +170,10 @@ public String getColor() { return color.getValue(); } + public int getFare() { + return fare.getValue(); + } + public List getStations() { return sections.getUpwards(); } diff --git a/src/main/java/subway/entity/LineEntity.java b/src/main/java/subway/entity/LineEntity.java index bc54e8618..8e3769b3f 100644 --- a/src/main/java/subway/entity/LineEntity.java +++ b/src/main/java/subway/entity/LineEntity.java @@ -8,19 +8,21 @@ public class LineEntity { private final Long id; private final String name; private final String color; + private final int fare; - public LineEntity(final String name, final String color) { - this(null, name, color); + public LineEntity(final String name, final String color, final int fare) { + this(null, name, color, fare); } - public LineEntity(final Long id, final String name, final String color) { + public LineEntity(final Long id, final String name, final String color, final int fare) { this.id = id; this.name = name; this.color = color; + this.fare = fare; } public static LineEntity from(final Line line) { - return new LineEntity(line.getId(), line.getName(), line.getColor()); + return new LineEntity(line.getId(), line.getName(), line.getColor(), line.getFare()); } public Long getId() { @@ -35,6 +37,10 @@ public String getColor() { return color; } + public int getFare() { + return fare; + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/src/main/java/subway/repository/LineRepository.java b/src/main/java/subway/repository/LineRepository.java index 3f0e1286c..9bb907638 100644 --- a/src/main/java/subway/repository/LineRepository.java +++ b/src/main/java/subway/repository/LineRepository.java @@ -25,7 +25,7 @@ public LineRepository(final LineDao lineDao, final SectionDao sectionDao) { public Line save(final Line line) { final LineEntity lineEntity = lineDao.save(LineEntity.from(line)); - return new Line(lineEntity.getId(), lineEntity.getName(), lineEntity.getColor()); + return new Line(lineEntity.getId(), lineEntity.getName(), lineEntity.getColor(), lineEntity.getFare()); } public Line findById(final Long lineId) { @@ -36,7 +36,12 @@ public Line findById(final Long lineId) { } private Line generateLine(final LineEntity lineEntity, final List sectionEntities) { - final Line line = new Line(lineEntity.getId(), lineEntity.getName(), lineEntity.getColor()); + final Line line = new Line( + lineEntity.getId(), + lineEntity.getName(), + lineEntity.getColor(), + lineEntity.getFare() + ); loadSections(line, generateSections(sectionEntities)); return line; } diff --git a/src/main/java/subway/service/LineService.java b/src/main/java/subway/service/LineService.java index 8030386e6..8b76a0cbc 100644 --- a/src/main/java/subway/service/LineService.java +++ b/src/main/java/subway/service/LineService.java @@ -27,7 +27,7 @@ public LineService(final LineRepository lineRepository, final StationRepository @Transactional public Long createLine(final LineCreateRequest request) { - final Line line = new Line(request.getName(), request.getColor()); + final Line line = new Line(request.getName(), request.getColor(), request.getFare()); return lineRepository.save(line).getId(); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index a7a4c1c3f..f0b85f685 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -10,8 +10,9 @@ CREATE TABLE IF NOT EXISTS LINE id BIGINT AUTO_INCREMENT NOT NULL, name VARCHAR(15) NOT NULL, color VARCHAR(15) NOT NULL, + fare INT NOT NULL, PRIMARY KEY (id) - ); +); CREATE TABLE IF NOT EXISTS SECTION ( diff --git a/src/main/resources/dummy.sql b/src/main/resources/dummy.sql index 983f7cd46..b29e4530d 100644 --- a/src/main/resources/dummy.sql +++ b/src/main/resources/dummy.sql @@ -1,5 +1,5 @@ -INSERT INTO line (id, name, color) VALUES (1, '2호선', '초록색'); +INSERT INTO line (id, name, color, fare) VALUES (1, '2호선', '초록색', 500); INSERT INTO station (id, name) VALUES (1, '잠실역'); INSERT INTO station (id, name) VALUES (2, '잠실새내역'); INSERT INTO section (id, line_id, upward_station_id, downward_station_id, distance) VALUES (1, 1, 1, 2, 10); diff --git a/src/test/java/subway/Integration/LineControllerIntegrationTest.java b/src/test/java/subway/Integration/LineControllerIntegrationTest.java index 42318c757..7712f9ad3 100644 --- a/src/test/java/subway/Integration/LineControllerIntegrationTest.java +++ b/src/test/java/subway/Integration/LineControllerIntegrationTest.java @@ -1,16 +1,17 @@ package subway.Integration; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasSize; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.nio.charset.Charset; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -18,8 +19,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; import subway.controller.dto.request.LineCreateRequest; import subway.controller.dto.request.SectionCreateRequest; +import subway.controller.dto.response.LineResponse; +import subway.controller.dto.response.LinesResponse; import subway.dao.StationDao; import subway.domain.line.Line; import subway.domain.station.Station; @@ -44,7 +48,7 @@ public class LineControllerIntegrationTest extends IntegrationTest { @BeforeEach void setUp() { - lineTwo = lineRepository.save(new Line("2호선", "초록색")); + lineTwo = lineRepository.save(new Line("2호선", "초록색", 500)); upward = stationRepository.save(new Station("잠실역")); downward = stationRepository.save(new Station("잠실새내역")); lineTwo.addSection(upward, downward, 10); @@ -54,32 +58,22 @@ void setUp() { @Test @DisplayName("노선 목록을 조회한다.") void findLines() throws Exception { - final Line lineFour = lineRepository.save(new Line("4호선", "하늘색")); + final Line lineFour = lineRepository.save(new Line("4호선", "하늘색", 1000)); final Station lineFourUpward = stationRepository.save(new Station("이수역")); final Station lineFourDownward = stationRepository.save(new Station("서울역")); lineFour.addSection(lineFourUpward, lineFourDownward, 10); lineRepository.update(lineFour); - mockMvc.perform(get("/lines")) + final MvcResult mvcResult = mockMvc.perform(get("/lines")) .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.lines", hasSize(2))) - .andExpect(jsonPath("$.lines[0].id").value(lineTwo.getId())) - .andExpect(jsonPath("$.lines[0].name").value("2호선")) - .andExpect(jsonPath("$.lines[0].color").value("초록색")) - .andExpect(jsonPath("$.lines[0].stations", hasSize(2))) - .andExpect(jsonPath("$.lines[0].stations[0].id").value(upward.getId())) - .andExpect(jsonPath("$.lines[0].stations[0].name").value("잠실역")) - .andExpect(jsonPath("$.lines[0].stations[1].id").value(downward.getId())) - .andExpect(jsonPath("$.lines[0].stations[1].name").value("잠실새내역")) - .andExpect(jsonPath("$.lines[1].id").value(lineFour.getId())) - .andExpect(jsonPath("$.lines[1].name").value("4호선")) - .andExpect(jsonPath("$.lines[1].color").value("하늘색")) - .andExpect(jsonPath("$.lines[1].stations", hasSize(2))) - .andExpect(jsonPath("$.lines[1].stations[0].id").value(lineFourUpward.getId())) - .andExpect(jsonPath("$.lines[1].stations[0].name").value("이수역")) - .andExpect(jsonPath("$.lines[1].stations[1].id").value(lineFourDownward.getId())) - .andExpect(jsonPath("$.lines[1].stations[1].name").value("서울역")); + .andReturn(); + + final LinesResponse response = + new LinesResponse(List.of(LineResponse.from(lineTwo), LineResponse.from(lineFour))); + final String jsonResponse = mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")); + final LinesResponse result = objectMapper.readValue(jsonResponse, LinesResponse.class); + assertThat(result).usingRecursiveComparison().isEqualTo(response); } @Nested @@ -89,7 +83,7 @@ class CreateLine { @Test @DisplayName("유효한 노선 정보라면 새로운 노선을 추가한다.") void createLine() throws Exception { - final LineCreateRequest request = new LineCreateRequest("2호선", "초록색"); + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색", 500); mockMvc.perform(post("/lines") .contentType(MediaType.APPLICATION_JSON) @@ -102,7 +96,7 @@ void createLine() throws Exception { @Test @DisplayName("이름이 공백이라면 400 상태를 반환한다.") void createLineWithInvalidName() throws Exception { - final LineCreateRequest request = new LineCreateRequest(" ", "초록색"); + final LineCreateRequest request = new LineCreateRequest(" ", "초록색", 500); mockMvc.perform(post("/lines") .contentType(MediaType.APPLICATION_JSON) @@ -115,7 +109,7 @@ void createLineWithInvalidName() throws Exception { @Test @DisplayName("색이 공백이라면 400 상태를 반환한다.") void createLineWithInvalidColor() throws Exception { - final LineCreateRequest request = new LineCreateRequest("2호선", " "); + final LineCreateRequest request = new LineCreateRequest("2호선", " ", 500); mockMvc.perform(post("/lines") .contentType(MediaType.APPLICATION_JSON) @@ -124,6 +118,32 @@ void createLineWithInvalidColor() throws Exception { .andExpect(status().isBadRequest()) .andExpect(content().string("노선 색깔은 공백일 수 없습니다.")); } + + @Test + @DisplayName("역 간의 거리가 입력되지 않으면 400 상태를 반환한다.") + void createSectionWithoutDistance() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(1L, 2L, null); + + mockMvc.perform(post("/lines/{id}/sections", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 간의 거리는 존재해야 합니다.")); + } + + @Test + @DisplayName("역 간의 거리가 0이하이면 400 상태를 반환한다.") + void createSectionWithNegativeDistance() throws Exception { + final SectionCreateRequest request = new SectionCreateRequest(1L, 2L, -1); + + mockMvc.perform(post("/lines/{id}/sections", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("역 간의 거리는 0보다 커야합니다.")); + } } @Nested @@ -133,17 +153,15 @@ class FindLine { @Test @DisplayName("존재하는 노선이라면 노선 정보를 조회한다.") void findLine() throws Exception { - mockMvc.perform(get("/lines/{id}", lineTwo.getId())) + final MvcResult mvcResult = mockMvc.perform(get("/lines/{id}", lineTwo.getId())) .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(lineTwo.getId())) - .andExpect(jsonPath("$.name").value("2호선")) - .andExpect(jsonPath("$.color").value("초록색")) - .andExpect(jsonPath("$.stations", hasSize(2))) - .andExpect(jsonPath("$.stations[0].id").value(upward.getId())) - .andExpect(jsonPath("$.stations[0].name").value("잠실역")) - .andExpect(jsonPath("$.stations[1].id").value(downward.getId())) - .andExpect(jsonPath("$.stations[1].name").value("잠실새내역")); + .andReturn(); + + final LineResponse response = LineResponse.from(lineTwo); + final String jsonResponse = mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")); + final LineResponse result = objectMapper.readValue(jsonResponse, LineResponse.class); + assertThat(result).usingRecursiveComparison().isEqualTo(response); } @Test diff --git a/src/test/java/subway/controller/LineControllerTest.java b/src/test/java/subway/controller/LineControllerTest.java index a8ed6663b..771bad546 100644 --- a/src/test/java/subway/controller/LineControllerTest.java +++ b/src/test/java/subway/controller/LineControllerTest.java @@ -1,7 +1,7 @@ package subway.controller; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasSize; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; @@ -12,10 +12,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.Charset; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -26,6 +26,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import subway.controller.dto.request.LineCreateRequest; import subway.controller.dto.request.SectionCreateRequest; import subway.controller.dto.response.LineResponse; @@ -57,32 +58,20 @@ void findLines() throws Exception { new StationResponse(4L, "서울역") ); final List lines = List.of( - new LineResponse(1L, "2호선", "초록색", stationsOfLineTwo), - new LineResponse(2L, "4호선", "하늘색", stationsOfLineFour)); + new LineResponse(1L, "2호선", "초록색", 500, stationsOfLineTwo), + new LineResponse(2L, "4호선", "하늘색", 1000, stationsOfLineFour)); final LinesResponse response = new LinesResponse(lines); given(lineService.findLines()).willReturn(response); - mockMvc.perform(get("/lines")) + final MvcResult mvcResult = mockMvc.perform(get("/lines")) .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.lines", hasSize(2))) - .andExpect(jsonPath("$.lines[0].id").value(1)) - .andExpect(jsonPath("$.lines[0].name").value("2호선")) - .andExpect(jsonPath("$.lines[0].color").value("초록색")) - .andExpect(jsonPath("$.lines[0].stations", hasSize(2))) - .andExpect(jsonPath("$.lines[0].stations[0].id").value(1)) - .andExpect(jsonPath("$.lines[0].stations[0].name").value("잠실역")) - .andExpect(jsonPath("$.lines[0].stations[1].id").value(2)) - .andExpect(jsonPath("$.lines[0].stations[1].name").value("잠실새내역")) - .andExpect(jsonPath("$.lines[1].id").value(2)) - .andExpect(jsonPath("$.lines[1].name").value("4호선")) - .andExpect(jsonPath("$.lines[1].color").value("하늘색")) - .andExpect(jsonPath("$.lines[1].stations", hasSize(2))) - .andExpect(jsonPath("$.lines[1].stations[0].id").value(3)) - .andExpect(jsonPath("$.lines[1].stations[0].name").value("이수역")) - .andExpect(jsonPath("$.lines[1].stations[1].id").value(4)) - .andExpect(jsonPath("$.lines[1].stations[1].name").value("서울역")); + .andReturn(); + + final String jsonResponse = mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")); + final LinesResponse result = objectMapper.readValue(jsonResponse, LinesResponse.class); + assertThat(result).usingRecursiveComparison().isEqualTo(response); } @Nested @@ -92,7 +81,7 @@ class CreateLine { @Test @DisplayName("유효한 노선 정보라면 새로운 노선을 추가한다.") void createLine() throws Exception { - final LineCreateRequest request = new LineCreateRequest("2호선", "초록색"); + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색", 500); given(lineService.createLine(any(LineCreateRequest.class))).willReturn(1L); @@ -105,9 +94,9 @@ void createLine() throws Exception { } @Test - @DisplayName("이름이 공백이라면 400 상태를 반환한다.") + @DisplayName("이름이 존재하지 않으면 400 상태를 반환한다.") void createLineWithInvalidName() throws Exception { - final LineCreateRequest request = new LineCreateRequest(" ", "초록색"); + final LineCreateRequest request = new LineCreateRequest(" ", "초록색", 500); mockMvc.perform(post("/lines") .contentType(MediaType.APPLICATION_JSON) @@ -118,9 +107,9 @@ void createLineWithInvalidName() throws Exception { } @Test - @DisplayName("색이 공백이라면 400 상태를 반환한다.") + @DisplayName("색이 존재하지 않으면 400 상태를 반환한다.") void createLineWithInvalidColor() throws Exception { - final LineCreateRequest request = new LineCreateRequest("2호선", " "); + final LineCreateRequest request = new LineCreateRequest("2호선", " ", 500); mockMvc.perform(post("/lines") .contentType(MediaType.APPLICATION_JSON) @@ -129,6 +118,32 @@ void createLineWithInvalidColor() throws Exception { .andExpect(status().isBadRequest()) .andExpect(content().string("노선 색깔은 공백일 수 없습니다.")); } + + @Test + @DisplayName("추가 요금이 존재하지 않으면 400 상태를 반환한다.") + void createLineWithNotExistFare() throws Exception { + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색", null); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("노선 추가 요금은 존재해야 합니다.")); + } + + @Test + @DisplayName("추가 요금이 0보다 작으면 400 상태를 반환한다.") + void createLineWithNegativeFare() throws Exception { + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색", -500); + + mockMvc.perform(post("/lines") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("노선 추가 요금은 0보다 커야합니다.")); + } } @Nested @@ -142,21 +157,18 @@ void findLine() throws Exception { new StationResponse(1L, "잠실역"), new StationResponse(2L, "잠실새내역") ); - final LineResponse response = new LineResponse(1L, "2호선", "초록색", stations); + final LineResponse response = new LineResponse(1L, "2호선", "초록색", 500, stations); given(lineService.findLineById(1L)).willReturn(response); - mockMvc.perform(get("/lines/{id}", 1L)) + final MvcResult mvcResult = mockMvc.perform(get("/lines/{id}", 1L)) .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1)) - .andExpect(jsonPath("$.name").value("2호선")) - .andExpect(jsonPath("$.color").value("초록색")) - .andExpect(jsonPath("$.stations", hasSize(2))) - .andExpect(jsonPath("$.stations[0].id").value(1)) - .andExpect(jsonPath("$.stations[0].name").value("잠실역")) - .andExpect(jsonPath("$.stations[1].id").value("2")) - .andExpect(jsonPath("$.stations[1].name").value("잠실새내역")); + .andReturn(); + + final String jsonResponse = mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")); + final LineResponse result = objectMapper.readValue(jsonResponse, LineResponse.class); + assertThat(result).usingRecursiveComparison().isEqualTo(response); } @Test diff --git a/src/test/java/subway/dao/LineDaoTest.java b/src/test/java/subway/dao/LineDaoTest.java index 454158d6d..bc66d1591 100644 --- a/src/test/java/subway/dao/LineDaoTest.java +++ b/src/test/java/subway/dao/LineDaoTest.java @@ -30,20 +30,12 @@ void setUp() { @Test @DisplayName("모든 노선을 조회한다.") void findAll() { - final LineEntity lineTwo = lineDao.save(new LineEntity("2호선", "초록색")); - final LineEntity lineFour = lineDao.save(new LineEntity("4호선", "하늘색")); + final LineEntity lineTwo = lineDao.save(new LineEntity("2호선", "초록색", 500)); + final LineEntity lineFour = lineDao.save(new LineEntity("4호선", "하늘색", 1000)); final List lines = lineDao.findAll(); - assertAll( - () -> assertThat(lines).hasSize(2), - () -> assertThat(lines.get(0).getId()).isEqualTo(lineTwo.getId()), - () -> assertThat(lines.get(0).getName()).isEqualTo(lineTwo.getName()), - () -> assertThat(lines.get(0).getColor()).isEqualTo(lineTwo.getColor()), - () -> assertThat(lines.get(1).getId()).isEqualTo(lineFour.getId()), - () -> assertThat(lines.get(1).getName()).isEqualTo(lineFour.getName()), - () -> assertThat(lines.get(1).getColor()).isEqualTo(lineFour.getColor()) - ); + assertThat(lines).usingRecursiveComparison().isEqualTo(List.of(lineTwo, lineFour)); } @Nested @@ -53,15 +45,11 @@ class FindById { @Test @DisplayName("존재하는 ID라면 노선 정보를 반환한다.") void findById() { - final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색", 500)); final Optional line = lineDao.findById(lineEntity.getId()); - assertAll( - () -> assertThat(line.get().getId()).isEqualTo(lineEntity.getId()), - () -> assertThat(line.get().getName()).isEqualTo(lineEntity.getName()), - () -> assertThat(line.get().getColor()).isEqualTo(lineEntity.getColor()) - ); + assertThat(line).usingRecursiveComparison().isEqualTo(Optional.of(lineEntity)); } @Test @@ -80,22 +68,22 @@ class Update { @Test @DisplayName("존재하는 노선이라면 정보를 업데이트한다.") void update() { - final LineEntity line = lineDao.save(new LineEntity("2호선", "초록색")); + final LineEntity line = lineDao.save(new LineEntity("2호선", "초록색", 500)); - final int numberOfUpdatedRow = lineDao.update(new LineEntity(line.getId(), "4호선", "하늘색")); + final LineEntity updateLine = new LineEntity(line.getId(), "4호선", "하늘색", 1000); + final int numberOfUpdatedRow = lineDao.update(updateLine); final LineEntity updatedLine = lineDao.findById(line.getId()).get(); assertAll( () -> assertThat(numberOfUpdatedRow).isEqualTo(1), - () -> assertThat(updatedLine.getName()).isEqualTo("4호선"), - () -> assertThat(updatedLine.getColor()).isEqualTo("하늘색") + () -> assertThat(updatedLine).usingRecursiveComparison().isEqualTo(updatedLine) ); } @Test @DisplayName("존재하지 않는 노선이라면 0을 반환한다.") void updateWithNotExistLine() { - final int numberOfUpdatedRow = lineDao.update(new LineEntity(1L, "4호선", "하늘색")); + final int numberOfUpdatedRow = lineDao.update(new LineEntity(1L, "4호선", "하늘색", 1000)); assertThat(numberOfUpdatedRow).isEqualTo(0); } diff --git a/src/test/java/subway/dao/SectionDaoTest.java b/src/test/java/subway/dao/SectionDaoTest.java index 5892e9758..6a9f0161d 100644 --- a/src/test/java/subway/dao/SectionDaoTest.java +++ b/src/test/java/subway/dao/SectionDaoTest.java @@ -1,7 +1,6 @@ package subway.dao; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -34,7 +33,7 @@ void setUp() { @Test @DisplayName("해당 노선의 모든 구간 정보를 조회한다.") void findAllByLineId() { - final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색", 500)); final StationEntity upward = stationDao.save(new StationEntity("잠실역")); final StationEntity downward = stationDao.save(new StationEntity("잠실새내역")); final SectionEntity entity = new SectionEntity(lineEntity.getId(), upward.getId(), downward.getId(), 10); @@ -48,7 +47,7 @@ void findAllByLineId() { @Test @DisplayName("모든 구간 정보를 저장한다.") void saveAll() { - final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색", 500)); final StationEntity upward = stationDao.save(new StationEntity("잠실역")); final StationEntity middle = stationDao.save(new StationEntity("잠실새내역")); final StationEntity downward = stationDao.save(new StationEntity("종합운동장역")); @@ -60,27 +59,13 @@ void saveAll() { sectionDao.saveAll(sections); final List result = sectionDao.findAllByLineId(lineEntity.getId()); - assertAll( - () -> assertThat(result).hasSize(2), - () -> assertThat(result.get(0).getLineId()).isEqualTo(lineEntity.getId()), - () -> assertThat(result.get(0).getUpwardStationId()).isEqualTo(upward.getId()), - () -> assertThat(result.get(0).getUpwardStation()).isEqualTo(upward.getName()), - () -> assertThat(result.get(0).getDownwardStationId()).isEqualTo(middle.getId()), - () -> assertThat(result.get(0).getDownwardStation()).isEqualTo(middle.getName()), - () -> assertThat(result.get(0).getDistance()).isEqualTo(10), - () -> assertThat(result.get(1).getLineId()).isEqualTo(lineEntity.getId()), - () -> assertThat(result.get(1).getUpwardStationId()).isEqualTo(middle.getId()), - () -> assertThat(result.get(1).getUpwardStation()).isEqualTo(middle.getName()), - () -> assertThat(result.get(1).getDownwardStationId()).isEqualTo(downward.getId()), - () -> assertThat(result.get(1).getDownwardStation()).isEqualTo(downward.getName()), - () -> assertThat(result.get(1).getDistance()).isEqualTo(10) - ); + assertThat(sections).usingRecursiveComparison().ignoringActualNullFields().isEqualTo(result); } @Test @DisplayName("노선의 구간 정보를 삭제한다.") void deleteAllByLineId() { - final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색")); + final LineEntity lineEntity = lineDao.save(new LineEntity("2호선", "초록색", 500)); final StationEntity upward = stationDao.save(new StationEntity("잠실역")); final StationEntity downward = stationDao.save(new StationEntity("잠실새내역")); final SectionEntity entity = new SectionEntity(lineEntity.getId(), upward.getId(), downward.getId(), 10); diff --git a/src/test/java/subway/dao/StationDaoTest.java b/src/test/java/subway/dao/StationDaoTest.java index 0863a86e8..21dde1d73 100644 --- a/src/test/java/subway/dao/StationDaoTest.java +++ b/src/test/java/subway/dao/StationDaoTest.java @@ -1,7 +1,6 @@ package subway.dao; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -37,10 +36,7 @@ void findById() { final Optional station = stationDao.findById(stationEntity.getId()); - assertAll( - () -> assertThat(station.get().getId()).isEqualTo(stationEntity.getId()), - () -> assertThat(station.get().getName()).isEqualTo(stationEntity.getName()) - ); + assertThat(station).usingRecursiveComparison().isEqualTo(Optional.of(stationEntity)); } @Test diff --git a/src/test/java/subway/domain/line/LineTest.java b/src/test/java/subway/domain/line/LineTest.java index b08963b16..b4e3907b2 100644 --- a/src/test/java/subway/domain/line/LineTest.java +++ b/src/test/java/subway/domain/line/LineTest.java @@ -29,7 +29,7 @@ void setUp() { new Section(upward, downward, 10), new Section(downward, Station.TERMINAL, 0) ); - line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + line = new Line(1L, "2호선", "초록색", 500, new ArrayList<>(sections)); } @Nested @@ -41,7 +41,7 @@ class AddSection { void addFirstSection() { final Station upward = new Station(1L, "잠실역"); final Station downward = new Station(2L, "종합운동장역"); - final Line line = new Line(1L, "2호선", "초록색"); + final Line line = new Line(1L, "2호선", "초록색", 500); line.addSection(upward, downward, 10); diff --git a/src/test/java/subway/repository/LineRepositoryTest.java b/src/test/java/subway/repository/LineRepositoryTest.java index faed86cf6..00161ea67 100644 --- a/src/test/java/subway/repository/LineRepositoryTest.java +++ b/src/test/java/subway/repository/LineRepositoryTest.java @@ -16,7 +16,6 @@ import subway.dao.SectionDao; import subway.dao.StationDao; import subway.domain.line.Line; -import subway.domain.section.Section; import subway.domain.station.Station; import subway.exception.InvalidLineException; @@ -41,14 +40,15 @@ void setUp() { @Test @DisplayName("노선을 저장한다.") void save() { - final Line line = new Line("2호선", "초록색"); + final Line line = new Line("2호선", "초록색", 500); final Line result = lineRepository.save(line); assertAll( () -> assertThat(result.getId()).isNotNull(), - () -> assertThat(result.getName()).isEqualTo("2호선"), - () -> assertThat(result.getColor()).isEqualTo("초록색") + () -> assertThat(result.getName()).isEqualTo(line.getName()), + () -> assertThat(result.getColor()).isEqualTo(line.getColor()), + () -> assertThat(result.getFare()).isEqualTo(line.getFare()) ); } @@ -60,7 +60,7 @@ class FindById { @BeforeEach void setUp() { - line = lineRepository.save(new Line("2호선", "초록색")); + line = lineRepository.save(new Line("2호선", "초록색", 500)); final Station upward = stationRepository.save(new Station("잠실역")); final Station downward = stationRepository.save(new Station("잠실새내역")); line.addSection(upward, downward, 10); @@ -72,16 +72,7 @@ void setUp() { void findById() { final Line result = lineRepository.findById(line.getId()); - final List
sections = result.getSections(); - assertAll( - () -> assertThat(result.getId()).isEqualTo(line.getId()), - () -> assertThat(result.getName()).isEqualTo(line.getName()), - () -> assertThat(result.getColor()).isEqualTo(line.getColor()), - () -> assertThat(sections).hasSize(1), - () -> assertThat(sections.get(0).getUpward().getName()).isEqualTo("잠실역"), - () -> assertThat(sections.get(0).getDownward().getName()).isEqualTo("잠실새내역"), - () -> assertThat(sections.get(0).getDistance()).isEqualTo(10) - ); + assertThat(line).usingRecursiveComparison().isEqualTo(result); } @Test @@ -97,16 +88,7 @@ void findByInvalidId() { void findAll() { final List lines = lineRepository.findAll(); - final List
sections = lines.get(0).getSections(); - assertAll( - () -> assertThat(lines).hasSize(1), - () -> assertThat(lines.get(0).getName()).isEqualTo("2호선"), - () -> assertThat(lines.get(0).getColor()).isEqualTo("초록색"), - () -> assertThat(sections).hasSize(1), - () -> assertThat(sections.get(0).getUpward().getName()).isEqualTo("잠실역"), - () -> assertThat(sections.get(0).getDownward().getName()).isEqualTo("잠실새내역"), - () -> assertThat(sections.get(0).getDistance()).isEqualTo(10) - ); + assertThat(lines).usingRecursiveComparison().isEqualTo(List.of(line)); } } @@ -117,7 +99,7 @@ class Update { @Test @DisplayName("섹션이 추가 됐을 때 노선 정보를 업데이트한다.") void updateWhenStationAdded() { - final Line line = lineRepository.save(new Line("2호선", "초록색")); + final Line line = lineRepository.save(new Line("2호선", "초록색", 500)); final Station upward = stationRepository.save(new Station("잠실역")); final Station middle = stationRepository.save(new Station("종합운동장역")); final Station downward = stationRepository.save(new Station("잠실새내역")); @@ -128,22 +110,14 @@ void updateWhenStationAdded() { lineRepository.update(line); final Line result = lineRepository.findById(line.getId()); - final List stations = result.getStations(); - assertAll( - () -> assertThat(stations).hasSize(3), - () -> assertThat(stations).extracting(Station::getName).containsExactly( - "잠실역", - "종합운동장역", - "잠실새내역" - ) - ); + assertThat(result).usingRecursiveComparison().isEqualTo(line); } @Test @DisplayName("섹션이 삭제 됐을 때 노선 정보를 업데이트한다.") void updateWhenStationDeleted() { - final Line line = lineRepository.save(new Line("2호선", "초록색")); + final Line line = lineRepository.save(new Line("2호선", "초록색", 500)); final Station upward = stationRepository.save(new Station("잠실역")); final Station downward = stationRepository.save(new Station("잠실새내역")); line.addSection(upward, downward, 10); diff --git a/src/test/java/subway/repository/StationRepositoryTest.java b/src/test/java/subway/repository/StationRepositoryTest.java index e0b6b49e1..7a7b08486 100644 --- a/src/test/java/subway/repository/StationRepositoryTest.java +++ b/src/test/java/subway/repository/StationRepositoryTest.java @@ -53,10 +53,7 @@ void findById() { final Station result = stationRepository.findById(station.getId()); - assertAll( - () -> assertThat(result.getId()).isEqualTo(station.getId()), - () -> assertThat(result.getName()).isEqualTo(station.getName()) - ); + assertThat(result).usingRecursiveComparison().isEqualTo(station); } @Test diff --git a/src/test/java/subway/service/LineServiceTest.java b/src/test/java/subway/service/LineServiceTest.java index e88455201..d982e60cd 100644 --- a/src/test/java/subway/service/LineServiceTest.java +++ b/src/test/java/subway/service/LineServiceTest.java @@ -2,13 +2,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -53,32 +53,17 @@ void findLines() { new Section(new Station(4L, "서울역"), Station.TERMINAL, 0) ); final List lines = List.of( - new Line(1L, "2호선", "초록색", sectionsOfLineTwo), - new Line(2L, "4호선", "하늘색", sectionsOfLineFour) + new Line(1L, "2호선", "초록색", 500, sectionsOfLineTwo), + new Line(2L, "4호선", "하늘색", 500, sectionsOfLineFour) ); given(lineRepository.findAll()).willReturn(lines); final LinesResponse response = lineService.findLines(); - assertAll( - () -> assertThat(response.getLines()).hasSize(2), - () -> assertThat(response.getLines().get(0).getId()).isEqualTo(1L), - () -> assertThat(response.getLines().get(0).getName()).isEqualTo("2호선"), - () -> assertThat(response.getLines().get(0).getColor()).isEqualTo("초록색"), - () -> assertThat(response.getLines().get(0).getStations()).hasSize(2), - () -> assertThat(response.getLines().get(0).getStations().get(0).getId()).isEqualTo(1L), - () -> assertThat(response.getLines().get(0).getStations().get(0).getName()).isEqualTo("잠실역"), - () -> assertThat(response.getLines().get(0).getStations().get(1).getId()).isEqualTo(2L), - () -> assertThat(response.getLines().get(0).getStations().get(1).getName()).isEqualTo("잠실새내역"), - () -> assertThat(response.getLines().get(1).getId()).isEqualTo(2L), - () -> assertThat(response.getLines().get(1).getName()).isEqualTo("4호선"), - () -> assertThat(response.getLines().get(1).getColor()).isEqualTo("하늘색"), - () -> assertThat(response.getLines().get(1).getStations()).hasSize(2), - () -> assertThat(response.getLines().get(1).getStations().get(0).getId()).isEqualTo(3L), - () -> assertThat(response.getLines().get(1).getStations().get(0).getName()).isEqualTo("이수역"), - () -> assertThat(response.getLines().get(1).getStations().get(1).getId()).isEqualTo(4L), - () -> assertThat(response.getLines().get(1).getStations().get(1).getName()).isEqualTo("서울역") - ); + final List lineResponses = lines.stream() + .map(LineResponse::from) + .collect(Collectors.toList()); + assertThat(response).usingRecursiveComparison().isEqualTo(new LinesResponse(lineResponses)); } @Nested @@ -88,8 +73,8 @@ class CreateLine { @Test @DisplayName("유효한 정보라면 노선을 추가한다.") void createLine() { - final Line line = new Line(1L, "2호선", "초록색"); - final LineCreateRequest request = new LineCreateRequest("2호선", "초록색"); + final Line line = new Line(1L, "2호선", "초록색", 500); + final LineCreateRequest request = new LineCreateRequest("2호선", "초록색", 500); given(lineRepository.save(any(Line.class))).willReturn(line); final Long lineId = lineService.createLine(request); @@ -100,7 +85,7 @@ void createLine() { @Test @DisplayName("유효하지 않은 정보라면 예외를 던진다.") void createLineWithInvalidName() { - final LineCreateRequest request = new LineCreateRequest("경의중앙선", "초록색"); + final LineCreateRequest request = new LineCreateRequest("경의중앙선", "초록색", 500); assertThatThrownBy(() -> lineService.createLine(request)) .isInstanceOf(InvalidLineNameException.class); @@ -118,21 +103,12 @@ void findLineById() { new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) ); - final Line line = new Line(1L, "2호선", "초록색", sections); + final Line line = new Line(1L, "2호선", "초록색", 500, sections); given(lineRepository.findById(1L)).willReturn(line); final LineResponse response = lineService.findLineById(1L); - assertAll( - () -> assertThat(response.getId()).isEqualTo(1L), - () -> assertThat(response.getName()).isEqualTo("2호선"), - () -> assertThat(response.getColor()).isEqualTo("초록색"), - () -> assertThat(response.getStations()).hasSize(2), - () -> assertThat(response.getStations().get(0).getId()).isEqualTo(1L), - () -> assertThat(response.getStations().get(0).getName()).isEqualTo("잠실역"), - () -> assertThat(response.getStations().get(1).getId()).isEqualTo(2L), - () -> assertThat(response.getStations().get(1).getName()).isEqualTo("잠실새내역") - ); + assertThat(response).usingRecursiveComparison().isEqualTo(LineResponse.from(line)); } } @@ -148,7 +124,7 @@ void createSection() { new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) ); - final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + final Line line = new Line(1L, "2호선", "초록색", 500, new ArrayList<>(sections)); given(lineRepository.findById(1L)).willReturn(line); given(stationRepository.findById(1L)).willReturn(new Station(1L, "잠실역")); given(stationRepository.findById(3L)).willReturn(new Station(3L, "종합운동장역")); @@ -167,7 +143,7 @@ void createSectionWithInvalidDistance() { new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) ); - final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + final Line line = new Line(1L, "2호선", "초록색", 500, new ArrayList<>(sections)); given(lineRepository.findById(1L)).willReturn(line); given(stationRepository.findById(1L)).willReturn(new Station(1L, "잠실역")); given(stationRepository.findById(3L)).willReturn(new Station(3L, "종합운동장역")); @@ -188,7 +164,7 @@ void deleteStation() { new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) ); - final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + final Line line = new Line(1L, "2호선", "초록색", 500, new ArrayList<>(sections)); given(lineRepository.findById(1L)).willReturn(line); given(stationRepository.findById(1L)).willReturn(new Station(1L, "잠실역")); willDoNothing().given(lineRepository).update(any(Line.class)); @@ -205,7 +181,7 @@ void deleteStationWithInvalidStationId() { new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10), new Section(new Station(2L, "잠실새내역"), Station.TERMINAL, 0) ); - final Line line = new Line(1L, "2호선", "초록색", new ArrayList<>(sections)); + final Line line = new Line(1L, "2호선", "초록색", 500, new ArrayList<>(sections)); given(lineRepository.findById(1L)).willReturn(line); given(stationRepository.findById(3L)).willReturn(new Station(3L, "종합운동장역")); diff --git a/src/test/java/subway/service/StationServiceTest.java b/src/test/java/subway/service/StationServiceTest.java index 21ca57263..44b42947e 100644 --- a/src/test/java/subway/service/StationServiceTest.java +++ b/src/test/java/subway/service/StationServiceTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.any; import static org.mockito.BDDMockito.given; @@ -69,10 +68,7 @@ void findStationById() { final StationResponse result = stationService.findStationById(1L); - assertAll( - () -> assertThat(result.getId()).isEqualTo(station.getId()), - () -> assertThat(result.getName()).isEqualTo(station.getName()) - ); + assertThat(result).usingRecursiveComparison().isEqualTo(StationResponse.from(station)); } @Test diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index a7a4c1c3f..f0b85f685 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -10,8 +10,9 @@ CREATE TABLE IF NOT EXISTS LINE id BIGINT AUTO_INCREMENT NOT NULL, name VARCHAR(15) NOT NULL, color VARCHAR(15) NOT NULL, + fare INT NOT NULL, PRIMARY KEY (id) - ); +); CREATE TABLE IF NOT EXISTS SECTION ( From d565e543824fdac93006934357bf0ea32fc20338 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 15:30:36 +0900 Subject: [PATCH 14/40] =?UTF-8?q?feat:=20=EB=85=B8=EC=84=A0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EA=B8=88=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/domain/line/Fare.java | 25 +++++++++++++++++++ .../java/subway/domain/line/FareTest.java | 19 ++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/main/java/subway/domain/line/Fare.java create mode 100644 src/test/java/subway/domain/line/FareTest.java diff --git a/src/main/java/subway/domain/line/Fare.java b/src/main/java/subway/domain/line/Fare.java new file mode 100644 index 000000000..a968ed3b8 --- /dev/null +++ b/src/main/java/subway/domain/line/Fare.java @@ -0,0 +1,25 @@ +package subway.domain.line; + +import subway.exception.InvalidFareException; + +public class Fare { + + private static final int MINIMUM_VALUE = 0; + + private final int value; + + public Fare(final int value) { + validate(value); + this.value = value; + } + + private void validate(final int value) { + if (value < MINIMUM_VALUE) { + throw new InvalidFareException("노선 추가 요금은 " + MINIMUM_VALUE + "원보다 커야 합니다."); + } + } + + public int getValue() { + return value; + } +} diff --git a/src/test/java/subway/domain/line/FareTest.java b/src/test/java/subway/domain/line/FareTest.java new file mode 100644 index 000000000..dc1486307 --- /dev/null +++ b/src/test/java/subway/domain/line/FareTest.java @@ -0,0 +1,19 @@ +package subway.domain.line; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import subway.exception.InvalidFareException; + +class FareTest { + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -1}) + @DisplayName("추가 요금이 0보다 작으면 예외를 던진다.") + void validate(final int value) { + Assertions.assertThatThrownBy(() -> new Fare(value)) + .isInstanceOf(InvalidFareException.class) + .hasMessage("노선 추가 요금은 0원보다 커야 합니다."); + } +} From 0bba461ac0a4d1589b1b28e8853556042f9d19c5 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 17:38:51 +0900 Subject: [PATCH 15/40] =?UTF-8?q?feat:=20=EA=B1=B0=EB=A6=AC=EB=B3=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EC=9A=94=EA=B8=88=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/fare/DistancePolicy.java | 33 +++++++++++++++++++ .../domain/fare/DistancePolicyTest.java | 32 ++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/main/java/subway/domain/fare/DistancePolicy.java create mode 100644 src/test/java/subway/domain/fare/DistancePolicyTest.java diff --git a/src/main/java/subway/domain/fare/DistancePolicy.java b/src/main/java/subway/domain/fare/DistancePolicy.java new file mode 100644 index 000000000..96bc4cdea --- /dev/null +++ b/src/main/java/subway/domain/fare/DistancePolicy.java @@ -0,0 +1,33 @@ +package subway.domain.fare; + +import java.util.function.Function; + +public enum DistancePolicy { + + BASE_FIFTY(50, distance -> { + final long additionDistance = distance - 50; + if (additionDistance <= 0) { + return 0L; + } + return (long) ((Math.ceil((additionDistance - 1) / 8) + 1) * 100); + }), + BASE_TEN(10, distance -> { + final long additionDistance = Math.min(BASE_FIFTY.base, distance) - 10; + if (additionDistance <= 0) { + return 0L; + } + return (long) ((Math.ceil((additionDistance - 1) / 5) + 1) * 100); + }); + + private final int base; + private final Function policy; + + DistancePolicy(final int base, final Function policy) { + this.base = base; + this.policy = policy; + } + + public long calculateAdditionFare(final long distance) { + return policy.apply(distance); + } +} diff --git a/src/test/java/subway/domain/fare/DistancePolicyTest.java b/src/test/java/subway/domain/fare/DistancePolicyTest.java new file mode 100644 index 000000000..45e1c262a --- /dev/null +++ b/src/test/java/subway/domain/fare/DistancePolicyTest.java @@ -0,0 +1,32 @@ +package subway.domain.fare; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class DistancePolicyTest { + + @ParameterizedTest + @CsvSource(value = {"12,100", "16,200", "50,800", "58,800"}) + @DisplayName("10km~50km는 5km마다 100원이 추가된다.") + void baseTen(final int distance, final long additionFare) { + final DistancePolicy distancePolicy = DistancePolicy.BASE_TEN; + + final long result = distancePolicy.calculateAdditionFare(distance); + + assertThat(result).isEqualTo(additionFare); + } + + @ParameterizedTest + @CsvSource(value = {"50,0", "55,100", "58,100", "66,200"}) + @DisplayName("50km부터는 8km마다 100원이 추가된다.") + void baseFifty(final int distance, final long additionFare) { + final DistancePolicy distancePolicy = DistancePolicy.BASE_FIFTY; + + final long result = distancePolicy.calculateAdditionFare(distance); + + assertThat(result).isEqualTo(additionFare); + } +} From 6e0d4c1646d428b23f3df1aee63d9abeb90c1b2b Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 19:17:57 +0900 Subject: [PATCH 16/40] =?UTF-8?q?feat:=20=EC=A7=80=ED=95=98=EC=B2=A0=20?= =?UTF-8?q?=EB=A7=B5=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EC=B5=9C=EB=8B=A8=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C,=20=EC=B5=9C=EB=8B=A8=20=EA=B1=B0=EB=A6=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/billing/BillingPolicy.java | 6 -- .../domain/billing/DistanceBillingPolicy.java | 27 --------- src/main/java/subway/domain/line/Lines.java | 16 ++++++ .../subway/domain/section/PathSection.java | 57 +++++++++++++++++++ .../domain/subway/LineWeightedEdge.java | 46 +++++++++++++++ src/main/java/subway/domain/subway/Path.java | 23 -------- .../subway/domain/subway/SubwayGraph.java | 3 +- .../domain/subway/SubwayJgraphtGraph.java | 52 ++++++++++++----- .../domain/subway/SubwayJgraphtGraphTest.java | 47 ++++++++------- 9 files changed, 186 insertions(+), 91 deletions(-) delete mode 100644 src/main/java/subway/domain/billing/BillingPolicy.java delete mode 100644 src/main/java/subway/domain/billing/DistanceBillingPolicy.java create mode 100644 src/main/java/subway/domain/line/Lines.java create mode 100644 src/main/java/subway/domain/section/PathSection.java create mode 100644 src/main/java/subway/domain/subway/LineWeightedEdge.java delete mode 100644 src/main/java/subway/domain/subway/Path.java diff --git a/src/main/java/subway/domain/billing/BillingPolicy.java b/src/main/java/subway/domain/billing/BillingPolicy.java deleted file mode 100644 index 242444d1c..000000000 --- a/src/main/java/subway/domain/billing/BillingPolicy.java +++ /dev/null @@ -1,6 +0,0 @@ -package subway.domain.billing; - -public interface BillingPolicy { - - int calculateFare(final int distance); -} diff --git a/src/main/java/subway/domain/billing/DistanceBillingPolicy.java b/src/main/java/subway/domain/billing/DistanceBillingPolicy.java deleted file mode 100644 index d18b8690c..000000000 --- a/src/main/java/subway/domain/billing/DistanceBillingPolicy.java +++ /dev/null @@ -1,27 +0,0 @@ -package subway.domain.billing; - -import subway.exception.InvalidDistanceException; - -public class DistanceBillingPolicy implements BillingPolicy { - - private static final int BASELINE_DISTANCE = 10; - private static final int BASELINE_FEE = 1250; - private static final int EXTRA_CHARGE_DISTANCE = 5; - private static final int EXTRA_CHARGE_FEE = 100; - - @Override - public int calculateFare(final int distance) { - if (distance < 0) { - throw new InvalidDistanceException("거리가 0보다 작은 경우 요금을 계산할 수 없습니다."); - } - if (distance <= BASELINE_DISTANCE) { - return BASELINE_FEE * (int) Math.ceil((double) distance / BASELINE_DISTANCE); - } - final int overDistance = distance - BASELINE_DISTANCE; - return BASELINE_FEE + calculateOverFare(overDistance); - } - - private int calculateOverFare(final int distance) { - return (int) ((Math.ceil((distance - 1) / EXTRA_CHARGE_DISTANCE) + 1) * EXTRA_CHARGE_FEE); - } -} diff --git a/src/main/java/subway/domain/line/Lines.java b/src/main/java/subway/domain/line/Lines.java new file mode 100644 index 000000000..82dea23f1 --- /dev/null +++ b/src/main/java/subway/domain/line/Lines.java @@ -0,0 +1,16 @@ +package subway.domain.line; + +import java.util.List; + +public class Lines { + + private final List lines; + + public Lines(final List lines) { + this.lines = lines; + } + + public List getLines() { + return lines; + } +} diff --git a/src/main/java/subway/domain/section/PathSection.java b/src/main/java/subway/domain/section/PathSection.java new file mode 100644 index 000000000..9300cb10e --- /dev/null +++ b/src/main/java/subway/domain/section/PathSection.java @@ -0,0 +1,57 @@ +package subway.domain.section; + +import subway.domain.station.Station; +import subway.domain.subway.LineWeightedEdge; + +public class PathSection { + + private final Long lineId; + private final Station source; + private final Station target; + private final double distance; + private final int fareOfLine; + + public PathSection( + final Long lineId, + final Station source, + final Station target, + final double distance, + final int fareOfLine + ) { + this.lineId = lineId; + this.source = source; + this.target = target; + this.distance = distance; + this.fareOfLine = fareOfLine; + } + + public static PathSection from(final LineWeightedEdge edge) { + return new PathSection( + edge.getLineId(), + edge.getSource(), + edge.getTarget(), + edge.getWeight(), + edge.getFareOfLine() + ); + } + + public Long getLineId() { + return lineId; + } + + public Station getSource() { + return source; + } + + public Station getTarget() { + return target; + } + + public double getDistance() { + return distance; + } + + public int getFareOfLine() { + return fareOfLine; + } +} diff --git a/src/main/java/subway/domain/subway/LineWeightedEdge.java b/src/main/java/subway/domain/subway/LineWeightedEdge.java new file mode 100644 index 000000000..201c5a38d --- /dev/null +++ b/src/main/java/subway/domain/subway/LineWeightedEdge.java @@ -0,0 +1,46 @@ +package subway.domain.subway; + +import org.jgrapht.graph.DefaultWeightedEdge; +import subway.domain.station.Station; + +public class LineWeightedEdge extends DefaultWeightedEdge { + + private Long lineId; + private int fareOfLine; + + public LineWeightedEdge() { + } + + public LineWeightedEdge(final Long lineId, final int fareOfLine) { + this.lineId = lineId; + this.fareOfLine = fareOfLine; + } + + public Station getSource() { + return (Station) super.getSource(); + } + + public Station getTarget() { + return (Station) super.getTarget(); + } + + public double getWeight() { + return super.getWeight(); + } + + public Long getLineId() { + return lineId; + } + + public int getFareOfLine() { + return fareOfLine; + } + + public void setLineId(final Long lineId) { + this.lineId = lineId; + } + + public void setFareOfLine(final int fareOfLine) { + this.fareOfLine = fareOfLine; + } +} diff --git a/src/main/java/subway/domain/subway/Path.java b/src/main/java/subway/domain/subway/Path.java deleted file mode 100644 index 313a1965e..000000000 --- a/src/main/java/subway/domain/subway/Path.java +++ /dev/null @@ -1,23 +0,0 @@ -package subway.domain.subway; - -import java.util.List; -import subway.domain.station.Station; - -public class Path { - - private final long distance; - private List stations; - - public Path(final long distance, final List stations) { - this.distance = distance; - this.stations = stations; - } - - public long getDistance() { - return distance; - } - - public List getStations() { - return stations; - } -} diff --git a/src/main/java/subway/domain/subway/SubwayGraph.java b/src/main/java/subway/domain/subway/SubwayGraph.java index 3bcacc2bf..89f692cc2 100644 --- a/src/main/java/subway/domain/subway/SubwayGraph.java +++ b/src/main/java/subway/domain/subway/SubwayGraph.java @@ -1,11 +1,12 @@ package subway.domain.subway; import java.util.List; +import subway.domain.section.PathSection; import subway.domain.station.Station; public interface SubwayGraph { - List findShortestPath(final Station start, final Station end); + List findShortestPathSections(final Station start, final Station end); long calculateShortestDistance(final Station start, final Station end); } diff --git a/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java b/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java index 048f6135f..8d8fd3f12 100644 --- a/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java +++ b/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java @@ -1,25 +1,29 @@ package subway.domain.subway; import java.util.List; +import java.util.stream.Collectors; import org.jgrapht.alg.shortestpath.DijkstraShortestPath; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.jgrapht.graph.WeightedMultigraph; +import org.jgrapht.graph.DefaultDirectedWeightedGraph; import subway.domain.line.Line; +import subway.domain.line.Lines; +import subway.domain.section.PathSection; import subway.domain.section.Section; import subway.domain.station.Station; import subway.exception.InvalidStationException; public class SubwayJgraphtGraph implements SubwayGraph { + private final Lines lines; private final DijkstraShortestPath dijkstraShortestPath; public SubwayJgraphtGraph(final List lines) { + this.lines = new Lines(lines); this.dijkstraShortestPath = new DijkstraShortestPath(generateGraph(lines)); } - private WeightedMultigraph generateGraph(final List lines) { - final WeightedMultigraph graph = - new WeightedMultigraph(DefaultWeightedEdge.class); + private DefaultDirectedWeightedGraph generateGraph(final List lines) { + final DefaultDirectedWeightedGraph graph = + new DefaultDirectedWeightedGraph<>(LineWeightedEdge.class); for (final Line line : lines) { drawGraph(graph, line); @@ -27,20 +31,42 @@ private WeightedMultigraph generateGraph(final Lis return graph; } - private void drawGraph(final WeightedMultigraph graph, final Line line) { + private void drawGraph(final DefaultDirectedWeightedGraph graph, final Line line) { for (final Section section : line.getSections()) { - final Station upward = section.getUpward(); - final Station downward = section.getDownward(); - graph.addVertex(upward); - graph.addVertex(downward); - graph.setEdgeWeight(graph.addEdge(upward, downward), section.getDistance()); + addVertex(graph, section); + addEdge(graph, line, section); } } + private void addVertex(final DefaultDirectedWeightedGraph graph, final Section section) { + graph.addVertex(section.getUpward()); + graph.addVertex(section.getDownward()); + } + + private void addEdge( + final DefaultDirectedWeightedGraph graph, + final Line line, + final Section section + ) { + final LineWeightedEdge upwardEdge = graph.addEdge(section.getUpward(), section.getDownward()); + final LineWeightedEdge downwardEdge = graph.addEdge(section.getDownward(), section.getUpward()); + + upwardEdge.setLineId(line.getId()); + upwardEdge.setFareOfLine(line.getFare()); + downwardEdge.setLineId(line.getId()); + downwardEdge.setFareOfLine(line.getFare()); + + graph.setEdgeWeight(upwardEdge, section.getDistance()); + graph.setEdgeWeight(downwardEdge, section.getDistance()); + } + @Override - public List findShortestPath(final Station start, final Station end) { + public List findShortestPathSections(final Station start, final Station end) { try { - return dijkstraShortestPath.getPath(start, end).getVertexList(); + final List edges = dijkstraShortestPath.getPath(start, end).getEdgeList(); + return edges.stream() + .map(PathSection::from) + .collect(Collectors.toList()); } catch (IllegalArgumentException e) { throw new InvalidStationException("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } diff --git a/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java b/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java index ba3b198cf..4a2ab6335 100644 --- a/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java +++ b/src/test/java/subway/domain/subway/SubwayJgraphtGraphTest.java @@ -13,54 +13,59 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import subway.domain.line.Line; -import subway.domain.station.Station; +import subway.domain.section.PathSection; import subway.exception.InvalidStationException; class SubwayJgraphtGraphTest { @Nested - @DisplayName("findShortestPath 메서드는 ") + @DisplayName("findShortestPathSections 메서드는 ") class GetShortestPath { @Test @DisplayName("호선 정보를 통해 최단 경로를 반환한다.") - void getShortestPath() { - final Line lineOfTwo = new Line(2L, "2호선", "초록색"); - final Line lineOfThree = new Line(3L, "3호선", "주황색"); - final Line lineOfNew = new Line(9L, "9호선", "빨간색"); + void findShortestPathSections() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); + final Line lineOfThree = new Line(3L, "3호선", "주황색", 800); + final Line lineOfNine = new Line(9L, "9호선", "빨간색", 1000); lineOfTwo.addSection(GYODAE, GANGNAM, 20); lineOfThree.addSection(GYODAE, NAMBU, 5); lineOfThree.addSection(NAMBU, YANGJAE, 5); - lineOfNew.addSection(GANGNAM, YANGJAE, 5); + lineOfNine.addSection(GANGNAM, YANGJAE, 5); - final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo, lineOfThree, lineOfNew)); - final List result = subwayGraph.findShortestPath(GYODAE, GANGNAM); + final SubwayJgraphtGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo, lineOfThree, lineOfNine)); + final List result = subwayGraph.findShortestPathSections(GYODAE, GANGNAM); - assertThat(result).containsExactly(GYODAE, NAMBU, YANGJAE, GANGNAM); + final List expected = List.of( + new PathSection(3L, GYODAE, NAMBU, 5, 800), + new PathSection(3L, NAMBU, YANGJAE, 5, 800), + new PathSection(9L, YANGJAE, GANGNAM, 5, 1000) + ); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); } @Test @DisplayName("출발역이 등록되어 있지 않은 경우 예외를 던진다.") - void getShortestPathWithInvalidStartStation() { - final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + void findShortestPathSectionsWithInvalidStartStation() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); lineOfTwo.addSection(GYODAE, GANGNAM, 20); final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); - assertThatThrownBy(() -> subwayGraph.findShortestPath(JAMSIL, GANGNAM)) + assertThatThrownBy(() -> subwayGraph.findShortestPathSections(JAMSIL, GANGNAM)) .isInstanceOf(InvalidStationException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } @Test @DisplayName("도착역이 등록되어 있지 않은 경우 예외를 던진다.") - void getShortestPathWithInvalidEndStation() { - final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + void findShortestPathSectionsWithInvalidEndStation() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); lineOfTwo.addSection(GYODAE, GANGNAM, 20); final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); - assertThatThrownBy(() -> subwayGraph.findShortestPath(GANGNAM, JAMSIL)) + assertThatThrownBy(() -> subwayGraph.findShortestPathSections(GANGNAM, JAMSIL)) .isInstanceOf(InvalidStationException.class) .hasMessage("노선 구간에 등록되지 않은 역 이름을 통해 경로를 조회할 수 없습니다."); } @@ -73,9 +78,9 @@ class GetShortestDistance { @Test @DisplayName("호선 정보를 통해 최단 거리를 반환한다.") void getShortestDistance() { - final Line lineOfTwo = new Line(2L, "2호선", "초록색"); - final Line lineOfThree = new Line(3L, "3호선", "주황색"); - final Line lineOfNew = new Line(9L, "9호선", "빨간색"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); + final Line lineOfThree = new Line(3L, "3호선", "주황색", 800); + final Line lineOfNew = new Line(9L, "9호선", "빨간색", 1000); lineOfTwo.addSection(GYODAE, GANGNAM, 20); lineOfThree.addSection(GYODAE, NAMBU, 5); lineOfThree.addSection(NAMBU, YANGJAE, 5); @@ -90,7 +95,7 @@ void getShortestDistance() { @Test @DisplayName("출발역이 등록되어 있지 않은 경우 예외를 던진다.") void getShortestDistanceWithInvalidStartStation() { - final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); lineOfTwo.addSection(GYODAE, GANGNAM, 20); final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); @@ -103,7 +108,7 @@ void getShortestDistanceWithInvalidStartStation() { @Test @DisplayName("도착역이 등록되어 있지 않은 경우 예외를 던진다.") void getShortestDistanceWithInvalidEndStation() { - final Line lineOfTwo = new Line(2L, "2호선", "초록색"); + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); lineOfTwo.addSection(GYODAE, GANGNAM, 20); final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo)); From 43152428d9bdee87b6dc3aab0323dea37ba6aa0a Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 19:41:14 +0900 Subject: [PATCH 17/40] =?UTF-8?q?feat:=20=EA=B1=B0=EB=A6=AC=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=9A=94=EA=B8=88=EC=9D=84=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/fare/DistanceFareStrategy.java | 20 ++++ .../java/subway/domain/fare/FareStrategy.java | 9 ++ .../java/subway/domain/subway/Passenger.java | 28 ++++++ .../java/subway/domain/subway/Subway.java | 7 +- .../exception/InvalidFareException.java | 8 ++ .../billing/DistanceBillingPolicyTest.java | 59 ------------ .../domain/fare/DistanceFareStrategyTest.java | 94 +++++++++++++++++++ .../java/subway/domain/subway/SubwayTest.java | 37 -------- 8 files changed, 161 insertions(+), 101 deletions(-) create mode 100644 src/main/java/subway/domain/fare/DistanceFareStrategy.java create mode 100644 src/main/java/subway/domain/fare/FareStrategy.java create mode 100644 src/main/java/subway/domain/subway/Passenger.java create mode 100644 src/main/java/subway/exception/InvalidFareException.java delete mode 100644 src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java create mode 100644 src/test/java/subway/domain/fare/DistanceFareStrategyTest.java delete mode 100644 src/test/java/subway/domain/subway/SubwayTest.java diff --git a/src/main/java/subway/domain/fare/DistanceFareStrategy.java b/src/main/java/subway/domain/fare/DistanceFareStrategy.java new file mode 100644 index 000000000..2066f0a19 --- /dev/null +++ b/src/main/java/subway/domain/fare/DistanceFareStrategy.java @@ -0,0 +1,20 @@ +package subway.domain.fare; + +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; + +public class DistanceFareStrategy implements FareStrategy { + + private static final long DEFAULT_FARE = 1250; + + @Override + public long calculateFare(final long fare, final Passenger passenger, final Subway subway) { + final long distance = subway.calculateShortestDistance(passenger.getStart(), passenger.getEnd()); + + long totalFare = DEFAULT_FARE; + for (final DistancePolicy distancePolicy : DistancePolicy.values()) { + totalFare += distancePolicy.calculateAdditionFare(distance); + } + return fare + totalFare; + } +} diff --git a/src/main/java/subway/domain/fare/FareStrategy.java b/src/main/java/subway/domain/fare/FareStrategy.java new file mode 100644 index 000000000..227c2860c --- /dev/null +++ b/src/main/java/subway/domain/fare/FareStrategy.java @@ -0,0 +1,9 @@ +package subway.domain.fare; + +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; + +public interface FareStrategy { + + long calculateFare(final long fare, final Passenger passenger, final Subway subway); +} diff --git a/src/main/java/subway/domain/subway/Passenger.java b/src/main/java/subway/domain/subway/Passenger.java new file mode 100644 index 000000000..98a21033c --- /dev/null +++ b/src/main/java/subway/domain/subway/Passenger.java @@ -0,0 +1,28 @@ +package subway.domain.subway; + +import subway.domain.station.Station; + +public class Passenger { + + private final int age; + private final Station start; + private final Station end; + + public Passenger(final int age, final Station start, final Station end) { + this.age = age; + this.start = start; + this.end = end; + } + + public int getAge() { + return age; + } + + public Station getStart() { + return start; + } + + public Station getEnd() { + return end; + } +} diff --git a/src/main/java/subway/domain/subway/Subway.java b/src/main/java/subway/domain/subway/Subway.java index 4e9bd6daf..91b3c185f 100644 --- a/src/main/java/subway/domain/subway/Subway.java +++ b/src/main/java/subway/domain/subway/Subway.java @@ -10,10 +10,7 @@ public Subway(final SubwayGraph subwayGraph) { this.subwayGraph = subwayGraph; } - public Path findShortestPath(final Station start, final Station end) { - return new Path( - subwayGraph.calculateShortestDistance(start, end), - subwayGraph.findShortestPath(start, end) - ); + public long calculateShortestDistance(final Station start, final Station end) { + return subwayGraph.calculateShortestDistance(start, end); } } diff --git a/src/main/java/subway/exception/InvalidFareException.java b/src/main/java/subway/exception/InvalidFareException.java new file mode 100644 index 000000000..c28a69ce9 --- /dev/null +++ b/src/main/java/subway/exception/InvalidFareException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class InvalidFareException extends SubwayException { + + public InvalidFareException(final String message) { + super(message); + } +} diff --git a/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java b/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java deleted file mode 100644 index 1d709d49d..000000000 --- a/src/test/java/subway/domain/billing/DistanceBillingPolicyTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package subway.domain.billing; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; -import subway.exception.InvalidDistanceException; - -class DistanceBillingPolicyTest { - - private static final DistanceBillingPolicy billingPolicy = new DistanceBillingPolicy(); - - @Nested - @DisplayName("calculateFee 메서드는 ") - class CalculateFee { - - @ParameterizedTest - @ValueSource(ints = {Integer.MIN_VALUE, -1}) - @DisplayName("거리가 0보다 작은 경우 예외를 던진다.") - void calculateFeeWithNegativeDistance(final int distance) { - assertThatThrownBy(() -> billingPolicy.calculateFare(distance)) - .isInstanceOf(InvalidDistanceException.class) - .hasMessage("거리가 0보다 작은 경우 요금을 계산할 수 없습니다."); - } - - @Test - @DisplayName("거리가 0인 경우 요금은 0원이다.") - void calculateFeeWithZeroDistance() { - final int distance = 0; - - final int result = billingPolicy.calculateFare(distance); - - assertThat(result).isEqualTo(0); - } - - @ParameterizedTest - @ValueSource(ints = {1, 5, 10}) - @DisplayName("거리가 기본운임 거리인 10km보다 작거나 같은 경우 요금은 기본요금인 1250원이다.") - void calculateFeeWithBaselineDistance(final int distance) { - final int result = billingPolicy.calculateFare(distance); - - assertThat(result).isEqualTo(1250); - } - - @ParameterizedTest - @CsvSource(value = {"12,1350", "16,1450", "58,2250"}) - @DisplayName("거리가 10km보다 클 때 5km마다 100원이 추가된 요금이 적용된다.") - void calculateFeeWithExtraDistance(final int distance, final int fee) { - final int result = billingPolicy.calculateFare(distance); - - assertThat(result).isEqualTo(fee); - } - } -} diff --git a/src/test/java/subway/domain/fare/DistanceFareStrategyTest.java b/src/test/java/subway/domain/fare/DistanceFareStrategyTest.java new file mode 100644 index 000000000..d463b889c --- /dev/null +++ b/src/test/java/subway/domain/fare/DistanceFareStrategyTest.java @@ -0,0 +1,94 @@ +package subway.domain.fare; + +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.YANGJAE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import subway.domain.line.Line; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; +import subway.domain.subway.SubwayJgraphtGraph; + +class DistanceFareStrategyTest { + + private static final DistanceFareStrategy distanceFareStrategy = new DistanceFareStrategy(); + + @Test + @DisplayName("거리가 10km 이내라면 요금은 기본운임 요금 1250원이다.") + void calculateFare() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); + lineOfTwo.addSection(GANGNAM, YANGJAE, 9); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); + + final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + + assertThat(result).isEqualTo(1250); + } + + @Nested + @DisplayName("거리가 10km~50km일 때 5km마다 100원을 추가한다면 ") + class BaseTenDistance { + + @Test + @DisplayName("거리가 12km라면 요금은 1350원이다.") + void distanceTwelve() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); + lineOfTwo.addSection(GANGNAM, YANGJAE, 12); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); + + final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + + assertThat(result).isEqualTo(1350); + } + + @Test + @DisplayName("거리가 16km라면 요금은 1450원이다.") + void distanceSixTeen() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); + lineOfTwo.addSection(GANGNAM, YANGJAE, 16); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); + + final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + + assertThat(result).isEqualTo(1450); + } + } + + @Nested + @DisplayName("거리가 50km 초과할 때 8km마다 100원을 추가한다면") + class BaseFiftyDistance { + + @Test + @DisplayName("거리가 58km라면 요금은 2150원이다.") + void distanceFiftyEight() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); + lineOfTwo.addSection(GANGNAM, YANGJAE, 58); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); + + final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + + assertThat(result).isEqualTo(2150); + } + + @Test + @DisplayName("거리가 66km라면 요금은 2250원이다.") + void distanceSixtySix() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 500); + lineOfTwo.addSection(GANGNAM, YANGJAE, 66); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); + + final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + + assertThat(result).isEqualTo(2250); + } + } +} diff --git a/src/test/java/subway/domain/subway/SubwayTest.java b/src/test/java/subway/domain/subway/SubwayTest.java deleted file mode 100644 index 46318b128..000000000 --- a/src/test/java/subway/domain/subway/SubwayTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package subway.domain.subway; - -import static fixtures.StationFixtures.GANGNAM; -import static fixtures.StationFixtures.GYODAE; -import static fixtures.StationFixtures.NAMBU; -import static fixtures.StationFixtures.YANGJAE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import subway.domain.line.Line; - -class SubwayTest { - - @Test - @DisplayName("최단 경로와 최단 거리를 확인한다.") - void findShortestPat() { - final Line lineOfTwo = new Line(2L, "2호선", "초록색"); - final Line lineOfThree = new Line(3L, "3호선", "주황색"); - final Line lineOfNew = new Line(9L, "9호선", "빨간색"); - lineOfTwo.addSection(GYODAE, GANGNAM, 20); - lineOfThree.addSection(GYODAE, NAMBU, 5); - lineOfThree.addSection(NAMBU, YANGJAE, 5); - lineOfNew.addSection(GANGNAM, YANGJAE, 5); - - final SubwayGraph subwayGraph = new SubwayJgraphtGraph(List.of(lineOfTwo, lineOfThree, lineOfNew)); - final Subway subway = new Subway(subwayGraph); - final Path result = subway.findShortestPath(GYODAE, GANGNAM); - - assertAll( - () -> assertThat(result.getStations()).containsExactly(GYODAE, NAMBU, YANGJAE, GANGNAM), - () -> assertThat(result.getDistance()).isEqualTo(15) - ); - } -} From 422ee0de16600bfea6b1c9760189dfd5e6b7ffc8 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 19:59:40 +0900 Subject: [PATCH 18/40] =?UTF-8?q?feat:=20=EB=85=B8=EC=84=A0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EA=B8=88=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=9A=94=EA=B8=88=EC=9D=84=20=EA=B3=84=EC=82=B0=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/fare/RouteFareStrategy.java | 22 ++++++++ .../java/subway/domain/subway/Subway.java | 6 +++ .../domain/fare/RouteFareStrategyTest.java | 50 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 src/main/java/subway/domain/fare/RouteFareStrategy.java create mode 100644 src/test/java/subway/domain/fare/RouteFareStrategyTest.java diff --git a/src/main/java/subway/domain/fare/RouteFareStrategy.java b/src/main/java/subway/domain/fare/RouteFareStrategy.java new file mode 100644 index 000000000..8e2a10584 --- /dev/null +++ b/src/main/java/subway/domain/fare/RouteFareStrategy.java @@ -0,0 +1,22 @@ +package subway.domain.fare; + +import java.util.List; +import subway.domain.section.PathSection; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; + +public class RouteFareStrategy implements FareStrategy { + + @Override + public long calculateFare(final long fare, final Passenger passenger, final Subway subway) { + final List sections = subway.findShortestPathSections(passenger.getStart(), passenger.getEnd()); + final int highestFareOfLine = findHighestFareOfLine(sections); + return fare + highestFareOfLine; + } + + private int findHighestFareOfLine(final List sections) { + return sections.stream() + .map(PathSection::getFareOfLine) + .reduce(0, Integer::max); + } +} diff --git a/src/main/java/subway/domain/subway/Subway.java b/src/main/java/subway/domain/subway/Subway.java index 91b3c185f..c2cf8d1a6 100644 --- a/src/main/java/subway/domain/subway/Subway.java +++ b/src/main/java/subway/domain/subway/Subway.java @@ -1,5 +1,7 @@ package subway.domain.subway; +import java.util.List; +import subway.domain.section.PathSection; import subway.domain.station.Station; public class Subway { @@ -10,6 +12,10 @@ public Subway(final SubwayGraph subwayGraph) { this.subwayGraph = subwayGraph; } + public List findShortestPathSections(final Station start, final Station end) { + return subwayGraph.findShortestPathSections(start, end); + } + public long calculateShortestDistance(final Station start, final Station end) { return subwayGraph.calculateShortestDistance(start, end); } diff --git a/src/test/java/subway/domain/fare/RouteFareStrategyTest.java b/src/test/java/subway/domain/fare/RouteFareStrategyTest.java new file mode 100644 index 000000000..80ff972d4 --- /dev/null +++ b/src/test/java/subway/domain/fare/RouteFareStrategyTest.java @@ -0,0 +1,50 @@ +package subway.domain.fare; + +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.GYODAE; +import static fixtures.StationFixtures.NAMBU; +import static fixtures.StationFixtures.YANGJAE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import subway.domain.line.Line; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; +import subway.domain.subway.SubwayJgraphtGraph; + +class RouteFareStrategyTest { + + private static final RouteFareStrategy routeFareStrategy = new RouteFareStrategy(); + + @Test + @DisplayName("900원 추가 요금이 있는 노선을 이용하면 기존 요금에 900원이 추가된다.") + void addition900() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 900); + lineOfTwo.addSection(GANGNAM, YANGJAE, 8); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); + + final long result = routeFareStrategy.calculateFare(1250, passenger, subway); + + assertThat(result).isEqualTo(2150); + } + + @Test + @DisplayName("0, 500, 900원의 추가 요금이 있는 노선을 경유하면 900원이 추가된다.") + void additionHighest900() { + final Line lineOfOne = new Line(1L, "1호선", "빨간색", 0); + final Line lineOfTwo = new Line(2L, "2호선", "노란색", 500); + final Line lineOfThree = new Line(3L, "3호선", "파란색", 900); + lineOfOne.addSection(GANGNAM, YANGJAE, 3); + lineOfTwo.addSection(YANGJAE, GYODAE, 3); + lineOfThree.addSection(GYODAE, NAMBU, 2); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfOne, lineOfTwo, lineOfThree))); + final Passenger passenger = new Passenger(26, GANGNAM, NAMBU); + + final long result = routeFareStrategy.calculateFare(1250, passenger, subway); + + assertThat(result).isEqualTo(2150); + } +} From b86b7bc662c9451f8e300b8e5de3ce878d2f1463 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 20:30:03 +0900 Subject: [PATCH 19/40] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=B4=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=ED=95=A0=EC=9D=B8=20=EA=B8=88=EC=95=A1=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/domain/fare/AgePolicy.java | 36 ++++++++++ .../exception/InvalidPolicyException.java | 8 +++ .../subway/domain/fare/AgePolicyTest.java | 66 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/main/java/subway/domain/fare/AgePolicy.java create mode 100644 src/main/java/subway/exception/InvalidPolicyException.java create mode 100644 src/test/java/subway/domain/fare/AgePolicyTest.java diff --git a/src/main/java/subway/domain/fare/AgePolicy.java b/src/main/java/subway/domain/fare/AgePolicy.java new file mode 100644 index 000000000..09592d824 --- /dev/null +++ b/src/main/java/subway/domain/fare/AgePolicy.java @@ -0,0 +1,36 @@ +package subway.domain.fare; + +import java.util.Arrays; +import java.util.function.Function; +import subway.exception.InvalidPolicyException; + +public enum AgePolicy { + + BABY(age -> age < 6, fare -> 0d), + KID(age -> 6 <= age && age < 13, fare -> fare * 0.5), + TEEN(age -> 13 <= age && age < 19, fare -> fare * 0.2), + ADULT(age -> age >= 19, fare -> 0d); + + private final Function validTarget; + private final Function discount; + + AgePolicy(final Function validTarget, final Function discount) { + this.validTarget = validTarget; + this.discount = discount; + } + + private boolean canBeApplied(final int age) { + return validTarget.apply(age); + } + + public double calculateDiscountFare(final double fare) { + return discount.apply(fare); + } + + public static AgePolicy search(final int age) { + return Arrays.stream(AgePolicy.values()) + .filter(agePolicy -> agePolicy.canBeApplied(age)) + .findFirst() + .orElseThrow(() -> new InvalidPolicyException("적용할 수 있는 정책이 존재하지 않습니다.")); + } +} diff --git a/src/main/java/subway/exception/InvalidPolicyException.java b/src/main/java/subway/exception/InvalidPolicyException.java new file mode 100644 index 000000000..3e46ab393 --- /dev/null +++ b/src/main/java/subway/exception/InvalidPolicyException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class InvalidPolicyException extends SubwayException { + + public InvalidPolicyException(final String message) { + super(message); + } +} diff --git a/src/test/java/subway/domain/fare/AgePolicyTest.java b/src/test/java/subway/domain/fare/AgePolicyTest.java new file mode 100644 index 000000000..c9604825a --- /dev/null +++ b/src/test/java/subway/domain/fare/AgePolicyTest.java @@ -0,0 +1,66 @@ +package subway.domain.fare; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class AgePolicyTest { + + @ParameterizedTest + @CsvSource(value = {"5,BABY", "8,KID", "15,TEEN", "19,ADULT"}) + @DisplayName("나이에 맞는 정책을 찾는다.") + void search(final int age, final AgePolicy expected) { + final AgePolicy result = AgePolicy.search(age); + + assertThat(result).isEqualTo(expected); + } + + @Nested + @DisplayName("할인 금액을 계산할 때 ") + class CalculateDiscountFare { + + @Test + @DisplayName("BABY는 할인 금액이 0원이다.") + void baby() { + final AgePolicy agePolicy = AgePolicy.BABY; + + final double result = agePolicy.calculateDiscountFare(1000); + + assertThat(result).isEqualTo(0); + } + + @Test + @DisplayName("KID는 50% 할인 금액을 반환한다.") + void kid() { + final AgePolicy agePolicy = AgePolicy.KID; + + final double result = agePolicy.calculateDiscountFare(1000); + + assertThat(result).isEqualTo(500); + } + + @Test + @DisplayName("TEEN은 20% 할인 금액을 반환한다.") + void teen() { + final AgePolicy agePolicy = AgePolicy.TEEN; + + final double result = agePolicy.calculateDiscountFare(1000); + + assertThat(result).isEqualTo(200); + } + + @Test + @DisplayName("ADULT는 할인 금액이 0원이다.") + void adult() { + final AgePolicy agePolicy = AgePolicy.ADULT; + + final double result = agePolicy.calculateDiscountFare(1000); + + assertThat(result).isEqualTo(0); + } + } +} From 2cfc94362b5fb60ede7bf14c111713086cd84b71 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 20:54:08 +0900 Subject: [PATCH 20/40] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=B4=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=9A=94=EA=B8=88=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/fare/AgeFareStrategy.java | 16 ++++ .../domain/fare/DistanceFareStrategy.java | 4 +- .../java/subway/domain/fare/FareStrategy.java | 2 +- .../subway/domain/fare/RouteFareStrategy.java | 2 +- .../domain/fare/AgeFareStrategyTest.java | 86 +++++++++++++++++++ .../domain/fare/DistanceFareStrategyTest.java | 10 +-- .../domain/fare/RouteFareStrategyTest.java | 4 +- 7 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 src/main/java/subway/domain/fare/AgeFareStrategy.java create mode 100644 src/test/java/subway/domain/fare/AgeFareStrategyTest.java diff --git a/src/main/java/subway/domain/fare/AgeFareStrategy.java b/src/main/java/subway/domain/fare/AgeFareStrategy.java new file mode 100644 index 000000000..cff8250ec --- /dev/null +++ b/src/main/java/subway/domain/fare/AgeFareStrategy.java @@ -0,0 +1,16 @@ +package subway.domain.fare; + +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; + +public class AgeFareStrategy implements FareStrategy { + + private static final int BASE_DEDUCTION = 350; + + @Override + public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { + final AgePolicy agePolicy = AgePolicy.search(passenger.getAge()); + final double fareAfterDeduction = Math.max(0, fare - BASE_DEDUCTION); + return Math.max(0, fareAfterDeduction - agePolicy.calculateDiscountFare(fareAfterDeduction)); + } +} diff --git a/src/main/java/subway/domain/fare/DistanceFareStrategy.java b/src/main/java/subway/domain/fare/DistanceFareStrategy.java index 2066f0a19..ca5ffebd5 100644 --- a/src/main/java/subway/domain/fare/DistanceFareStrategy.java +++ b/src/main/java/subway/domain/fare/DistanceFareStrategy.java @@ -5,10 +5,10 @@ public class DistanceFareStrategy implements FareStrategy { - private static final long DEFAULT_FARE = 1250; + private static final int DEFAULT_FARE = 1250; @Override - public long calculateFare(final long fare, final Passenger passenger, final Subway subway) { + public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { final long distance = subway.calculateShortestDistance(passenger.getStart(), passenger.getEnd()); long totalFare = DEFAULT_FARE; diff --git a/src/main/java/subway/domain/fare/FareStrategy.java b/src/main/java/subway/domain/fare/FareStrategy.java index 227c2860c..67949f1c6 100644 --- a/src/main/java/subway/domain/fare/FareStrategy.java +++ b/src/main/java/subway/domain/fare/FareStrategy.java @@ -5,5 +5,5 @@ public interface FareStrategy { - long calculateFare(final long fare, final Passenger passenger, final Subway subway); + double calculateFare(final double fare, final Passenger passenger, final Subway subway); } diff --git a/src/main/java/subway/domain/fare/RouteFareStrategy.java b/src/main/java/subway/domain/fare/RouteFareStrategy.java index 8e2a10584..c0d50b74f 100644 --- a/src/main/java/subway/domain/fare/RouteFareStrategy.java +++ b/src/main/java/subway/domain/fare/RouteFareStrategy.java @@ -8,7 +8,7 @@ public class RouteFareStrategy implements FareStrategy { @Override - public long calculateFare(final long fare, final Passenger passenger, final Subway subway) { + public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { final List sections = subway.findShortestPathSections(passenger.getStart(), passenger.getEnd()); final int highestFareOfLine = findHighestFareOfLine(sections); return fare + highestFareOfLine; diff --git a/src/test/java/subway/domain/fare/AgeFareStrategyTest.java b/src/test/java/subway/domain/fare/AgeFareStrategyTest.java new file mode 100644 index 000000000..6df92d864 --- /dev/null +++ b/src/test/java/subway/domain/fare/AgeFareStrategyTest.java @@ -0,0 +1,86 @@ +package subway.domain.fare; + +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.YANGJAE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import subway.domain.line.Line; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; +import subway.domain.subway.SubwayJgraphtGraph; + +class AgeFareStrategyTest { + + private static final AgeFareStrategy ageFareStrategy = new AgeFareStrategy(); + + @ParameterizedTest + @ValueSource(ints = {-500, 0}) + @DisplayName("이전 요금이 0보다 작거나 같으면 요금은 없다.") + void zero(final int fare) { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); + lineOfTwo.addSection(GANGNAM, YANGJAE, 8); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(5, GANGNAM, YANGJAE); + + final double result = ageFareStrategy.calculateFare(fare, passenger, subway); + + assertThat(result).isEqualTo(0); + } + + @Test + @DisplayName("6세 미만 어린이인 경우 할인이 적용되지 않는다.") + void baby() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); + lineOfTwo.addSection(GANGNAM, YANGJAE, 8); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(5, GANGNAM, YANGJAE); + + final double result = ageFareStrategy.calculateFare(1350, passenger, subway); + + assertThat(result).isEqualTo(1000); + } + + @Test + @DisplayName("6세 이상, 13세 미만의 어린이인 경우 50% 할인이 적용된다.") + void kid() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); + lineOfTwo.addSection(GANGNAM, YANGJAE, 8); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(8, GANGNAM, YANGJAE); + + final double result = ageFareStrategy.calculateFare(1350, passenger, subway); + + assertThat(result).isEqualTo(500); + } + + @Test + @DisplayName("13세 이상, 19세 미만 청소년인 경우 20% 할인이 적용된다.") + void teen() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); + lineOfTwo.addSection(GANGNAM, YANGJAE, 8); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(15, GANGNAM, YANGJAE); + + final double result = ageFareStrategy.calculateFare(1350, passenger, subway); + + assertThat(result).isEqualTo(800); + } + + @Test + @DisplayName("19세 이상 어른인 경우 할인이 적용되지 않는다.") + void adult() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); + lineOfTwo.addSection(GANGNAM, YANGJAE, 8); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(19, GANGNAM, YANGJAE); + + final double result = ageFareStrategy.calculateFare(1350, passenger, subway); + + assertThat(result).isEqualTo(1000); + } +} diff --git a/src/test/java/subway/domain/fare/DistanceFareStrategyTest.java b/src/test/java/subway/domain/fare/DistanceFareStrategyTest.java index d463b889c..afa81a265 100644 --- a/src/test/java/subway/domain/fare/DistanceFareStrategyTest.java +++ b/src/test/java/subway/domain/fare/DistanceFareStrategyTest.java @@ -25,7 +25,7 @@ void calculateFare() { final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); - final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + final double result = distanceFareStrategy.calculateFare(0, passenger, subway); assertThat(result).isEqualTo(1250); } @@ -42,7 +42,7 @@ void distanceTwelve() { final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); - final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + final double result = distanceFareStrategy.calculateFare(0, passenger, subway); assertThat(result).isEqualTo(1350); } @@ -55,7 +55,7 @@ void distanceSixTeen() { final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); - final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + final double result = distanceFareStrategy.calculateFare(0, passenger, subway); assertThat(result).isEqualTo(1450); } @@ -73,7 +73,7 @@ void distanceFiftyEight() { final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); - final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + final double result = distanceFareStrategy.calculateFare(0, passenger, subway); assertThat(result).isEqualTo(2150); } @@ -86,7 +86,7 @@ void distanceSixtySix() { final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); - final long result = distanceFareStrategy.calculateFare(0, passenger, subway); + final double result = distanceFareStrategy.calculateFare(0, passenger, subway); assertThat(result).isEqualTo(2250); } diff --git a/src/test/java/subway/domain/fare/RouteFareStrategyTest.java b/src/test/java/subway/domain/fare/RouteFareStrategyTest.java index 80ff972d4..426202d07 100644 --- a/src/test/java/subway/domain/fare/RouteFareStrategyTest.java +++ b/src/test/java/subway/domain/fare/RouteFareStrategyTest.java @@ -26,7 +26,7 @@ void addition900() { final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); final Passenger passenger = new Passenger(26, GANGNAM, YANGJAE); - final long result = routeFareStrategy.calculateFare(1250, passenger, subway); + final double result = routeFareStrategy.calculateFare(1250, passenger, subway); assertThat(result).isEqualTo(2150); } @@ -43,7 +43,7 @@ void additionHighest900() { final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfOne, lineOfTwo, lineOfThree))); final Passenger passenger = new Passenger(26, GANGNAM, NAMBU); - final long result = routeFareStrategy.calculateFare(1250, passenger, subway); + final double result = routeFareStrategy.calculateFare(1250, passenger, subway); assertThat(result).isEqualTo(2150); } From 5e2c0ad2d94d3dd243c6d826989ef22b66f6e80a Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 21:10:49 +0900 Subject: [PATCH 21/40] =?UTF-8?q?feat:=20=EB=B3=B5=ED=95=A9=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20=EC=A0=95=EC=B1=85=EC=9D=84=20=EC=88=98=ED=96=89?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/fare/FareStrategyComposite.java | 23 ++++++++++++ .../fare/FareStrategyCompositeTest.java | 37 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/subway/domain/fare/FareStrategyComposite.java create mode 100644 src/test/java/subway/domain/fare/FareStrategyCompositeTest.java diff --git a/src/main/java/subway/domain/fare/FareStrategyComposite.java b/src/main/java/subway/domain/fare/FareStrategyComposite.java new file mode 100644 index 000000000..8877b3d8b --- /dev/null +++ b/src/main/java/subway/domain/fare/FareStrategyComposite.java @@ -0,0 +1,23 @@ +package subway.domain.fare; + +import java.util.List; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; + +public class FareStrategyComposite implements FareStrategy { + + private final List strategies; + + public FareStrategyComposite(final List strategies) { + this.strategies = strategies; + } + + @Override + public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { + double baseFare = fare; + for (final FareStrategy strategy : strategies) { + baseFare = strategy.calculateFare(baseFare, passenger, subway); + } + return baseFare; + } +} diff --git a/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java b/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java new file mode 100644 index 000000000..cc00b9716 --- /dev/null +++ b/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java @@ -0,0 +1,37 @@ +package subway.domain.fare; + +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.YANGJAE; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import subway.domain.line.Line; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; +import subway.domain.subway.SubwayJgraphtGraph; + +class FareStrategyCompositeTest { + + private static final FareStrategyComposite fareStrategyComposite = new FareStrategyComposite( + List.of( + new DistanceFareStrategy(), + new RouteFareStrategy(), + new AgeFareStrategy() + ) + ); + + @Test + @DisplayName("지정된 요금 정책을 수행 후 요금을 확인한다.") + void calculateFare() { + final Line lineOfTwo = new Line(2L, "2호선", "초록색", 1000); + lineOfTwo.addSection(GANGNAM, YANGJAE, 12); + final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); + final Passenger passenger = new Passenger(15, GANGNAM, YANGJAE); + + final double result = fareStrategyComposite.calculateFare(0, passenger, subway); + + assertThat(result).isEqualTo(1600); + } +} From c5a393cb34b1bf4fd30b7e5d4908e3ad30d69360 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Wed, 17 May 2023 21:20:20 +0900 Subject: [PATCH 22/40] =?UTF-8?q?refactor:=20=EB=85=B8=EC=84=A0=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4,=20=ED=83=91=EC=8A=B9=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/controller/SubwayController.java | 4 +- .../dto/request/LineCreateRequest.java | 2 +- ...FindRequest.java => PassengerRequest.java} | 16 ++++++-- .../subway/controller/LineControllerTest.java | 2 +- .../controller/SubwayControllerTest.java | 39 ++++++++++++++++--- 5 files changed, 51 insertions(+), 12 deletions(-) rename src/main/java/subway/controller/dto/request/{ShortestPathFindRequest.java => PassengerRequest.java} (53%) diff --git a/src/main/java/subway/controller/SubwayController.java b/src/main/java/subway/controller/SubwayController.java index 23cc33005..564074da8 100644 --- a/src/main/java/subway/controller/SubwayController.java +++ b/src/main/java/subway/controller/SubwayController.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import subway.controller.dto.request.ShortestPathFindRequest; +import subway.controller.dto.request.PassengerRequest; import subway.controller.dto.response.ShortestPathResponse; import subway.service.SubwayService; @@ -21,7 +21,7 @@ public SubwayController(final SubwayService subwayService) { } @GetMapping("/shortest-path") - public ResponseEntity findShortestPath(@Valid @RequestBody ShortestPathFindRequest request) { + public ResponseEntity findShortestPath(@Valid @RequestBody PassengerRequest request) { final ShortestPathResponse response = subwayService.findShortestPath(request); return ResponseEntity.ok(response); } diff --git a/src/main/java/subway/controller/dto/request/LineCreateRequest.java b/src/main/java/subway/controller/dto/request/LineCreateRequest.java index d4813f649..e30ccf7fe 100644 --- a/src/main/java/subway/controller/dto/request/LineCreateRequest.java +++ b/src/main/java/subway/controller/dto/request/LineCreateRequest.java @@ -17,7 +17,7 @@ public class LineCreateRequest { @Schema(description = "노선 추가 요금") @NotNull(message = "노선 추가 요금은 존재해야 합니다.") - @Min(value = 0, message = "노선 추가 요금은 0보다 커야합니다.") + @Min(value = 0, message = "노선 추가 요금은 0원 이상 가능합니다.") private Integer fare; private LineCreateRequest() { diff --git a/src/main/java/subway/controller/dto/request/ShortestPathFindRequest.java b/src/main/java/subway/controller/dto/request/PassengerRequest.java similarity index 53% rename from src/main/java/subway/controller/dto/request/ShortestPathFindRequest.java rename to src/main/java/subway/controller/dto/request/PassengerRequest.java index e3b80015e..d7719e443 100644 --- a/src/main/java/subway/controller/dto/request/ShortestPathFindRequest.java +++ b/src/main/java/subway/controller/dto/request/PassengerRequest.java @@ -1,8 +1,13 @@ package subway.controller.dto.request; +import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; -public class ShortestPathFindRequest { +public class PassengerRequest { + + @NotNull(message = "탑승자 나이는 입력해야 합니다.") + @Min(value = 1, message = "탑승자 나이는 0보다 커야합니다.") + private Integer age; @NotNull(message = "출발역 ID는 존재해야 합니다.") private Long startStationId; @@ -10,14 +15,19 @@ public class ShortestPathFindRequest { @NotNull(message = "도착역 ID는 존재해야 합니다.") private Long endStationId; - private ShortestPathFindRequest() { + private PassengerRequest() { } - public ShortestPathFindRequest(final Long startStationId, final Long endStationId) { + public PassengerRequest(final Integer age, final Long startStationId, final Long endStationId) { + this.age = age; this.startStationId = startStationId; this.endStationId = endStationId; } + public Integer getAge() { + return age; + } + public Long getStartStationId() { return startStationId; } diff --git a/src/test/java/subway/controller/LineControllerTest.java b/src/test/java/subway/controller/LineControllerTest.java index 771bad546..88ae898d7 100644 --- a/src/test/java/subway/controller/LineControllerTest.java +++ b/src/test/java/subway/controller/LineControllerTest.java @@ -142,7 +142,7 @@ void createLineWithNegativeFare() throws Exception { .content(objectMapper.writeValueAsString(request))) .andDo(print()) .andExpect(status().isBadRequest()) - .andExpect(content().string("노선 추가 요금은 0보다 커야합니다.")); + .andExpect(content().string("노선 추가 요금은 0원 이상 가능합니다.")); } } diff --git a/src/test/java/subway/controller/SubwayControllerTest.java b/src/test/java/subway/controller/SubwayControllerTest.java index 10dc2daeb..78c63ea9d 100644 --- a/src/test/java/subway/controller/SubwayControllerTest.java +++ b/src/test/java/subway/controller/SubwayControllerTest.java @@ -15,13 +15,15 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import subway.controller.dto.request.ShortestPathFindRequest; +import subway.controller.dto.request.PassengerRequest; import subway.controller.dto.response.ShortestPathResponse; import subway.controller.dto.response.StationResponse; import subway.service.SubwayService; @@ -45,7 +47,7 @@ class FindShortestPath { @Test @DisplayName("유효한 요청이라면 최단 경로 정보를 반환한다.") void findShortestPath() throws Exception { - final ShortestPathFindRequest request = new ShortestPathFindRequest(1L, 2L); + final PassengerRequest request = new PassengerRequest(10, 1L, 2L); final ShortestPathResponse response = new ShortestPathResponse( List.of( StationResponse.from(StationFixtures.GANGNAM), @@ -55,7 +57,7 @@ void findShortestPath() throws Exception { 1250 ); - given(subwayService.findShortestPath(any(ShortestPathFindRequest.class))).willReturn(response); + given(subwayService.findShortestPath(any(PassengerRequest.class))).willReturn(response); final MvcResult mvcResult = mockMvc.perform(get("/subways/shortest-path") .contentType(MediaType.APPLICATION_JSON) @@ -72,7 +74,7 @@ void findShortestPath() throws Exception { @Test @DisplayName("출발역 ID가 존재하지 않으면 400 상태를 반환한다.") void findShortestPathWithInvalidStartStation() throws Exception { - final ShortestPathFindRequest request = new ShortestPathFindRequest(null, 2L); + final PassengerRequest request = new PassengerRequest(10, null, 2L); mockMvc.perform(get("/subways/shortest-path") .contentType(MediaType.APPLICATION_JSON) @@ -85,7 +87,7 @@ void findShortestPathWithInvalidStartStation() throws Exception { @Test @DisplayName("도착역 ID가 존재하지 않으면 400 상태를 반환한다.") void findShortestPathWithInvalidEndStation() throws Exception { - final ShortestPathFindRequest request = new ShortestPathFindRequest(1L, null); + final PassengerRequest request = new PassengerRequest(10, 1L, null); mockMvc.perform(get("/subways/shortest-path") .contentType(MediaType.APPLICATION_JSON) @@ -94,5 +96,32 @@ void findShortestPathWithInvalidEndStation() throws Exception { .andExpect(status().isBadRequest()) .andExpect(content().string("도착역 ID는 존재해야 합니다.")); } + + @Test + @DisplayName("탑승자 나이가 존재하지 않으면 400 상태를 반환한다.") + void findShortestPathWithEmptyAge() throws Exception { + final PassengerRequest request = new PassengerRequest(null, 1L, 2L); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("탑승자 나이는 입력해야 합니다.")); + } + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -1, 0}) + @DisplayName("탑승자 나이가 0보다 작거나 같으면 400 상태를 반환한다.") + void findShortestPathWithInvalidAge(final int age) throws Exception { + final PassengerRequest request = new PassengerRequest(age, 1L, 2L); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("탑승자 나이는 0보다 커야합니다.")); + } } } From 5721896e5cd1ae29975d15a45ca848524ebf7edd Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 00:17:17 +0900 Subject: [PATCH 23/40] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EA=B3=A0=EC=9E=90=20=ED=95=98=EB=8A=94=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/LineSectionResponse.java | 37 +++++++++++ .../dto/response/SectionResponse.java | 37 +++++++++++ .../dto/response/ShortestPathResponse.java | 63 +++++++++++++++---- .../subway/domain/section/PathSection.java | 8 +-- .../controller/SubwayControllerTest.java | 12 ++-- 5 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 src/main/java/subway/controller/dto/response/LineSectionResponse.java create mode 100644 src/main/java/subway/controller/dto/response/SectionResponse.java diff --git a/src/main/java/subway/controller/dto/response/LineSectionResponse.java b/src/main/java/subway/controller/dto/response/LineSectionResponse.java new file mode 100644 index 000000000..1cc1cd419 --- /dev/null +++ b/src/main/java/subway/controller/dto/response/LineSectionResponse.java @@ -0,0 +1,37 @@ +package subway.controller.dto.response; + +import java.util.ArrayList; +import java.util.List; +import subway.domain.section.PathSection; + +public class LineSectionResponse { + + private Long lineId; + private List sections; + + public LineSectionResponse(final Long lineId, final List sections) { + this.lineId = lineId; + this.sections = sections; + } + + public static LineSectionResponse from(final List sections) { + return new LineSectionResponse(sections.get(0).getLineId(), generateSections(sections)); + } + + private static List generateSections(final List sections) { + final List result = new ArrayList<>(); + + for (final PathSection section : sections) { + result.add(SectionResponse.from(section)); + } + return result; + } + + public Long getLineId() { + return lineId; + } + + public List getSections() { + return sections; + } +} diff --git a/src/main/java/subway/controller/dto/response/SectionResponse.java b/src/main/java/subway/controller/dto/response/SectionResponse.java new file mode 100644 index 000000000..399a295d1 --- /dev/null +++ b/src/main/java/subway/controller/dto/response/SectionResponse.java @@ -0,0 +1,37 @@ +package subway.controller.dto.response; + +import subway.domain.section.PathSection; + +public class SectionResponse { + + private String upwardStationName; + private String downwardStationName; + private int distance; + + public SectionResponse(final String upwardStationName, final String downwardStationName, final int distance) { + this.upwardStationName = upwardStationName; + this.downwardStationName = downwardStationName; + this.distance = distance; + } + + public static SectionResponse from(final PathSection pathSection) { + return new SectionResponse( + pathSection.getSource().getName(), + pathSection.getTarget().getName(), + pathSection.getDistance() + ); + } + + public String getUpwardStationName() { + return upwardStationName; + } + + public String getDownwardStationName() { + return downwardStationName; + } + + public int getDistance() { + return distance; + } +} + diff --git a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java index abd67c43e..18907dfd6 100644 --- a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java +++ b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java @@ -1,28 +1,69 @@ package subway.controller.dto.response; +import java.util.ArrayList; import java.util.List; +import subway.domain.section.PathSection; public class ShortestPathResponse { - private List path; - private long distance; - private long fare; + private int transferCount; + private List path; + private long totalDistance; + private long subwayFare; - public ShortestPathResponse(final List path, final long distance, final long fare) { + public ShortestPathResponse( + final int transferCount, + final List path, + final long totalDistance, + final long subwayFare + ) { + this.transferCount = transferCount; this.path = path; - this.distance = distance; - this.fare = fare; + this.totalDistance = totalDistance; + this.subwayFare = subwayFare; } - public List getPath() { + public static ShortestPathResponse of( + final List sections, + final long totalDistance, + final long subwayFare + ) { + final List path = generateLineSections(sections); + return new ShortestPathResponse(path.size() - 1, path, totalDistance, subwayFare); + } + + private static List generateLineSections(final List sections) { + final List result = new ArrayList<>(); + final Long currentLineId = sections.get(0).getLineId(); + + List currentSections = new ArrayList<>(); + for (final PathSection section : sections) { + if (section.getLineId() != currentLineId) { + result.add(LineSectionResponse.from(currentSections)); + currentSections = new ArrayList<>(); + } + currentSections.add(section); + } + + if (!currentSections.isEmpty()) { + result.add(LineSectionResponse.from(currentSections)); + } + return result; + } + + public int getTransferCount() { + return transferCount; + } + + public List getPath() { return path; } - public long getDistance() { - return distance; + public long getTotalDistance() { + return totalDistance; } - public long getFare() { - return fare; + public long getSubwayFare() { + return subwayFare; } } diff --git a/src/main/java/subway/domain/section/PathSection.java b/src/main/java/subway/domain/section/PathSection.java index 9300cb10e..bef4d993a 100644 --- a/src/main/java/subway/domain/section/PathSection.java +++ b/src/main/java/subway/domain/section/PathSection.java @@ -8,14 +8,14 @@ public class PathSection { private final Long lineId; private final Station source; private final Station target; - private final double distance; + private final int distance; private final int fareOfLine; public PathSection( final Long lineId, final Station source, final Station target, - final double distance, + final int distance, final int fareOfLine ) { this.lineId = lineId; @@ -30,7 +30,7 @@ public static PathSection from(final LineWeightedEdge edge) { edge.getLineId(), edge.getSource(), edge.getTarget(), - edge.getWeight(), + (int) edge.getWeight(), edge.getFareOfLine() ); } @@ -47,7 +47,7 @@ public Station getTarget() { return target; } - public double getDistance() { + public int getDistance() { return distance; } diff --git a/src/test/java/subway/controller/SubwayControllerTest.java b/src/test/java/subway/controller/SubwayControllerTest.java index 78c63ea9d..fe25413d6 100644 --- a/src/test/java/subway/controller/SubwayControllerTest.java +++ b/src/test/java/subway/controller/SubwayControllerTest.java @@ -1,5 +1,7 @@ package subway.controller; +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.YANGJAE; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; @@ -9,7 +11,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import fixtures.StationFixtures; import java.nio.charset.Charset; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -25,7 +26,7 @@ import org.springframework.test.web.servlet.MvcResult; import subway.controller.dto.request.PassengerRequest; import subway.controller.dto.response.ShortestPathResponse; -import subway.controller.dto.response.StationResponse; +import subway.domain.section.PathSection; import subway.service.SubwayService; @WebMvcTest(SubwayController.class) @@ -48,13 +49,12 @@ class FindShortestPath { @DisplayName("유효한 요청이라면 최단 경로 정보를 반환한다.") void findShortestPath() throws Exception { final PassengerRequest request = new PassengerRequest(10, 1L, 2L); - final ShortestPathResponse response = new ShortestPathResponse( + final ShortestPathResponse response = ShortestPathResponse.of( List.of( - StationResponse.from(StationFixtures.GANGNAM), - StationResponse.from(StationFixtures.YANGJAE) + new PathSection(1L, GANGNAM, YANGJAE, 10, 1000) ), 10, - 1250 + 100 ); given(subwayService.findShortestPath(any(PassengerRequest.class))).willReturn(response); From b9b7435fdf0f5bec1e002875184c8e349c0bef60 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 00:31:23 +0900 Subject: [PATCH 24/40] =?UTF-8?q?feat:=20=EA=B2=BD=EB=A1=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/config/FareConfiguration.java | 25 +++++++ .../java/subway/service/SubwayService.java | 60 +++++++++++++++++ .../subway/service/SubwayServiceTest.java | 67 +++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/main/java/subway/config/FareConfiguration.java create mode 100644 src/main/java/subway/service/SubwayService.java create mode 100644 src/test/java/subway/service/SubwayServiceTest.java diff --git a/src/main/java/subway/config/FareConfiguration.java b/src/main/java/subway/config/FareConfiguration.java new file mode 100644 index 000000000..d3bc31713 --- /dev/null +++ b/src/main/java/subway/config/FareConfiguration.java @@ -0,0 +1,25 @@ +package subway.config; + +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import subway.domain.fare.AgeFareStrategy; +import subway.domain.fare.DistanceFareStrategy; +import subway.domain.fare.FareStrategy; +import subway.domain.fare.FareStrategyComposite; +import subway.domain.fare.RouteFareStrategy; + +@Configuration +public class FareConfiguration { + + @Bean + public FareStrategy fareStrategy() { + return new FareStrategyComposite( + List.of( + new DistanceFareStrategy(), + new RouteFareStrategy(), + new AgeFareStrategy() + ) + ); + } +} diff --git a/src/main/java/subway/service/SubwayService.java b/src/main/java/subway/service/SubwayService.java new file mode 100644 index 000000000..01350112f --- /dev/null +++ b/src/main/java/subway/service/SubwayService.java @@ -0,0 +1,60 @@ +package subway.service; + +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import subway.controller.dto.request.PassengerRequest; +import subway.controller.dto.response.ShortestPathResponse; +import subway.domain.fare.FareStrategy; +import subway.domain.line.Line; +import subway.domain.section.PathSection; +import subway.domain.station.Station; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; +import subway.domain.subway.SubwayGraph; +import subway.domain.subway.SubwayJgraphtGraph; +import subway.repository.LineRepository; +import subway.repository.StationRepository; + +@Transactional(readOnly = true) +@Service +public class SubwayService { + + private final LineRepository lineRepository; + private final StationRepository stationRepository; + private final FareStrategy fareStrategy; + + public SubwayService( + final LineRepository lineRepository, + final StationRepository stationRepository, + final FareStrategy fareStrategy + ) { + this.lineRepository = lineRepository; + this.stationRepository = stationRepository; + this.fareStrategy = fareStrategy; + } + + public ShortestPathResponse findShortestPath(final PassengerRequest request) { + final List lines = lineRepository.findAll(); + final Subway subway = generateSubway(lines); + final Passenger passenger = generatePassenger(request); + + final List pathSections = + subway.findShortestPathSections(passenger.getStart(), passenger.getEnd()); + final long totalDistance = subway.calculateShortestDistance(passenger.getStart(), passenger.getEnd()); + final long subwayFare = (long) fareStrategy.calculateFare(0, passenger, subway); + + return ShortestPathResponse.of(pathSections, totalDistance, subwayFare); + } + + private Subway generateSubway(final List lines) { + final SubwayGraph subwayGraph = new SubwayJgraphtGraph(lines); + return new Subway(subwayGraph); + } + + private Passenger generatePassenger(final PassengerRequest request) { + final Station start = stationRepository.findById(request.getStartStationId()); + final Station end = stationRepository.findById(request.getEndStationId()); + return new Passenger(request.getAge(), start, end); + } +} diff --git a/src/test/java/subway/service/SubwayServiceTest.java b/src/test/java/subway/service/SubwayServiceTest.java new file mode 100644 index 000000000..6f45530cf --- /dev/null +++ b/src/test/java/subway/service/SubwayServiceTest.java @@ -0,0 +1,67 @@ +package subway.service; + +import static fixtures.StationFixtures.GANGNAM; +import static fixtures.StationFixtures.GYODAE; +import static fixtures.StationFixtures.YANGJAE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.BDDMockito.given; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import subway.controller.dto.request.PassengerRequest; +import subway.controller.dto.response.ShortestPathResponse; +import subway.domain.fare.FareStrategy; +import subway.domain.line.Line; +import subway.domain.section.PathSection; +import subway.domain.subway.Passenger; +import subway.domain.subway.Subway; +import subway.repository.LineRepository; +import subway.repository.StationRepository; + +@ExtendWith(MockitoExtension.class) +class SubwayServiceTest { + + @InjectMocks + private SubwayService subwayService; + + @Mock + private LineRepository lineRepository; + @Mock + private StationRepository stationRepository; + @Mock + private FareStrategy fareStrategy; + + @Test + @DisplayName("탑승자 정보를 바탕으로 경로 정보를 반환한다.") + void findShortestPath() { + final PassengerRequest request = new PassengerRequest(8, 1L, 3L); + final Line lineOfOne = new Line(1L, "1호선", "빨간색", 500); + final Line lineOfTwo = new Line(2L, "2호선", "파란색", 1000); + lineOfOne.addSection(GANGNAM, YANGJAE, 5); + lineOfTwo.addSection(YANGJAE, GYODAE, 7); + + given(lineRepository.findAll()).willReturn(List.of(lineOfOne, lineOfTwo)); + given(stationRepository.findById(1L)).willReturn(GANGNAM); + given(stationRepository.findById(3L)).willReturn(GYODAE); + given(fareStrategy.calculateFare(anyDouble(), any(Passenger.class), any(Subway.class))).willReturn(1000d); + + final ShortestPathResponse result = subwayService.findShortestPath(request); + + final ShortestPathResponse expected = ShortestPathResponse.of( + List.of( + new PathSection(1L, GANGNAM, YANGJAE, 5, 500), + new PathSection(2L, YANGJAE, GYODAE, 7, 1000) + ), + 12, + 1000 + ); + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } +} From b182c26d34c02be2d75b008fbbecd0b1382eed5b Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 00:40:56 +0900 Subject: [PATCH 25/40] =?UTF-8?q?test:=20=EA=B2=BD=EB=A1=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SubwayControllerIntegrationTest.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/test/java/subway/Integration/SubwayControllerIntegrationTest.java diff --git a/src/test/java/subway/Integration/SubwayControllerIntegrationTest.java b/src/test/java/subway/Integration/SubwayControllerIntegrationTest.java new file mode 100644 index 000000000..89470b726 --- /dev/null +++ b/src/test/java/subway/Integration/SubwayControllerIntegrationTest.java @@ -0,0 +1,122 @@ +package subway.Integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.nio.charset.Charset; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import subway.controller.dto.request.PassengerRequest; +import subway.controller.dto.response.ShortestPathResponse; +import subway.domain.line.Line; +import subway.domain.section.PathSection; +import subway.domain.station.Station; +import subway.repository.LineRepository; +import subway.repository.StationRepository; + +class SubwayControllerIntegrationTest extends IntegrationTest { + + @Autowired + private LineRepository lineRepository; + + @Autowired + private StationRepository stationRepository; + + @Nested + @DisplayName("findShortestPath 메서드는 ") + class FindShortestPath { + + @Test + @DisplayName("유효한 요청이라면 최단 경로 정보를 반환한다.") + void findShortestPath() throws Exception { + final Line line = lineRepository.save(new Line(1L, "1호선", "빨간색", 1000)); + final Station upward = stationRepository.save(new Station("강남역")); + final Station downward = stationRepository.save(new Station("양재역")); + line.addSection(upward, downward, 10); + lineRepository.update(line); + + final PassengerRequest request = new PassengerRequest(10, upward.getId(), downward.getId()); + final ShortestPathResponse response = ShortestPathResponse.of( + List.of( + new PathSection(line.getId(), upward, downward, 10, 1000) + ), + 10, + 950 + ); + + final MvcResult mvcResult = mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn(); + + final String jsonResponse = mvcResult.getResponse().getContentAsString(Charset.forName("UTF-8")); + final ShortestPathResponse result = objectMapper.readValue(jsonResponse, ShortestPathResponse.class); + assertThat(result).usingRecursiveComparison().isEqualTo(response); + } + + @Test + @DisplayName("출발역 ID가 존재하지 않으면 400 상태를 반환한다.") + void findShortestPathWithInvalidStartStation() throws Exception { + final PassengerRequest request = new PassengerRequest(10, null, 2L); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("출발역 ID는 존재해야 합니다.")); + } + + @Test + @DisplayName("도착역 ID가 존재하지 않으면 400 상태를 반환한다.") + void findShortestPathWithInvalidEndStation() throws Exception { + final PassengerRequest request = new PassengerRequest(10, 1L, null); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("도착역 ID는 존재해야 합니다.")); + } + + @Test + @DisplayName("탑승자 나이가 존재하지 않으면 400 상태를 반환한다.") + void findShortestPathWithEmptyAge() throws Exception { + final PassengerRequest request = new PassengerRequest(null, 1L, 2L); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("탑승자 나이는 입력해야 합니다.")); + } + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -1, 0}) + @DisplayName("탑승자 나이가 0보다 작거나 같으면 400 상태를 반환한다.") + void findShortestPathWithInvalidAge(final int age) throws Exception { + final PassengerRequest request = new PassengerRequest(age, 1L, 2L); + + mockMvc.perform(get("/subways/shortest-path") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(content().string("탑승자 나이는 0보다 커야합니다.")); + } + } +} From 9b7471a630a5974cb26137ab8b96ee20b38b5d7e Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 00:49:17 +0900 Subject: [PATCH 26/40] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/domain/line/Lines.java | 16 ---------------- .../subway/domain/subway/SubwayJgraphtGraph.java | 3 --- 2 files changed, 19 deletions(-) delete mode 100644 src/main/java/subway/domain/line/Lines.java diff --git a/src/main/java/subway/domain/line/Lines.java b/src/main/java/subway/domain/line/Lines.java deleted file mode 100644 index 82dea23f1..000000000 --- a/src/main/java/subway/domain/line/Lines.java +++ /dev/null @@ -1,16 +0,0 @@ -package subway.domain.line; - -import java.util.List; - -public class Lines { - - private final List lines; - - public Lines(final List lines) { - this.lines = lines; - } - - public List getLines() { - return lines; - } -} diff --git a/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java b/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java index 8d8fd3f12..1141876bb 100644 --- a/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java +++ b/src/main/java/subway/domain/subway/SubwayJgraphtGraph.java @@ -5,7 +5,6 @@ import org.jgrapht.alg.shortestpath.DijkstraShortestPath; import org.jgrapht.graph.DefaultDirectedWeightedGraph; import subway.domain.line.Line; -import subway.domain.line.Lines; import subway.domain.section.PathSection; import subway.domain.section.Section; import subway.domain.station.Station; @@ -13,11 +12,9 @@ public class SubwayJgraphtGraph implements SubwayGraph { - private final Lines lines; private final DijkstraShortestPath dijkstraShortestPath; public SubwayJgraphtGraph(final List lines) { - this.lines = new Lines(lines); this.dijkstraShortestPath = new DijkstraShortestPath(generateGraph(lines)); } From 10d83f835fce5bf9944e0b76a115a8346175fbb4 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 00:50:58 +0900 Subject: [PATCH 27/40] =?UTF-8?q?docs:=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++++++ src/main/java/subway/controller/SubwayController.java | 4 ++++ .../subway/controller/dto/request/PassengerRequest.java | 4 ++++ .../controller/dto/response/LineSectionResponse.java | 4 ++++ .../subway/controller/dto/response/SectionResponse.java | 6 ++++++ .../controller/dto/response/ShortestPathResponse.java | 8 ++++++++ 6 files changed, 35 insertions(+) diff --git a/README.md b/README.md index 03c81b2a0..d58dd1577 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,12 @@ http://localhost:8080/swagger-ui/index.html#/ - 이용 거리 초과 시 추가운임 부과 - 10km~50km: 5km 까지 마다 100원 추가 - 50km 초과: 8km 까지 마다 100원 추가 + +- [x] 노선에 따른 요금을 계산한다. + - 추가 요금이 있는 노선을 이용하면 측정된 요금에 추가한다. + - 경로 중 추가 요금이 있는 노선을 환승하여 이용하면 가장 높은 금액의 추가 요금만 적용한다. + +- [x] 연령에 따른 요금 할인을 계산한다. + - 연령에 따른 요금 할인 정책을 반영한다. + - 청소년: 운임에서 350원을 공제한 금액의 20% 할인 + - 어린이: 운임에서 350원을 공제한 금액의 50% 할인 diff --git a/src/main/java/subway/controller/SubwayController.java b/src/main/java/subway/controller/SubwayController.java index 564074da8..b6c4326bc 100644 --- a/src/main/java/subway/controller/SubwayController.java +++ b/src/main/java/subway/controller/SubwayController.java @@ -1,5 +1,7 @@ package subway.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import javax.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,6 +12,7 @@ import subway.controller.dto.response.ShortestPathResponse; import subway.service.SubwayService; +@Tag(name = "Subway", description = "지하철 API Document") @RequestMapping("/subways") @RestController public class SubwayController { @@ -20,6 +23,7 @@ public SubwayController(final SubwayService subwayService) { this.subwayService = subwayService; } + @Operation(summary = "경로 정보 조회 API", description = "출발역에서 도착역까지의 경로 정보를 조회합니다.") @GetMapping("/shortest-path") public ResponseEntity findShortestPath(@Valid @RequestBody PassengerRequest request) { final ShortestPathResponse response = subwayService.findShortestPath(request); diff --git a/src/main/java/subway/controller/dto/request/PassengerRequest.java b/src/main/java/subway/controller/dto/request/PassengerRequest.java index d7719e443..f5f4bebaf 100644 --- a/src/main/java/subway/controller/dto/request/PassengerRequest.java +++ b/src/main/java/subway/controller/dto/request/PassengerRequest.java @@ -1,17 +1,21 @@ package subway.controller.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; public class PassengerRequest { + @Schema(description = "탑승자 나이") @NotNull(message = "탑승자 나이는 입력해야 합니다.") @Min(value = 1, message = "탑승자 나이는 0보다 커야합니다.") private Integer age; + @Schema(description = "출발역 ID") @NotNull(message = "출발역 ID는 존재해야 합니다.") private Long startStationId; + @Schema(description = "도착역 ID") @NotNull(message = "도착역 ID는 존재해야 합니다.") private Long endStationId; diff --git a/src/main/java/subway/controller/dto/response/LineSectionResponse.java b/src/main/java/subway/controller/dto/response/LineSectionResponse.java index 1cc1cd419..546ce99ea 100644 --- a/src/main/java/subway/controller/dto/response/LineSectionResponse.java +++ b/src/main/java/subway/controller/dto/response/LineSectionResponse.java @@ -1,12 +1,16 @@ package subway.controller.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.ArrayList; import java.util.List; import subway.domain.section.PathSection; public class LineSectionResponse { + @Schema(description = "노선 ID") private Long lineId; + + @Schema(description = "구간 정보 목록") private List sections; public LineSectionResponse(final Long lineId, final List sections) { diff --git a/src/main/java/subway/controller/dto/response/SectionResponse.java b/src/main/java/subway/controller/dto/response/SectionResponse.java index 399a295d1..af3093195 100644 --- a/src/main/java/subway/controller/dto/response/SectionResponse.java +++ b/src/main/java/subway/controller/dto/response/SectionResponse.java @@ -1,11 +1,17 @@ package subway.controller.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import subway.domain.section.PathSection; public class SectionResponse { + @Schema(description = "상행역 이름") private String upwardStationName; + + @Schema(description = "하행역 이름") private String downwardStationName; + + @Schema(description = "상행역과 하행역 사이의 거리") private int distance; public SectionResponse(final String upwardStationName, final String downwardStationName, final int distance) { diff --git a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java index 18907dfd6..66bdf07e6 100644 --- a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java +++ b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java @@ -1,14 +1,22 @@ package subway.controller.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.ArrayList; import java.util.List; import subway.domain.section.PathSection; public class ShortestPathResponse { + @Schema(description = "환승 횟수") private int transferCount; + + @Schema(description = "구간 경로 목록") private List path; + + @Schema(description = "출발역에서 도착역까지의 총 거리") private long totalDistance; + + @Schema(description = "출발역에서 도착역까지 운임 요금") private long subwayFare; public ShortestPathResponse( From 4539c99be99e4a8da909b430f139162335b40f62 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 00:52:55 +0900 Subject: [PATCH 28/40] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/domain/fare/AgePolicy.java | 14 +++++++------- .../subway/domain/subway/LineWeightedEdge.java | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/subway/domain/fare/AgePolicy.java b/src/main/java/subway/domain/fare/AgePolicy.java index 09592d824..48aaecc29 100644 --- a/src/main/java/subway/domain/fare/AgePolicy.java +++ b/src/main/java/subway/domain/fare/AgePolicy.java @@ -19,6 +19,13 @@ public enum AgePolicy { this.discount = discount; } + public static AgePolicy search(final int age) { + return Arrays.stream(AgePolicy.values()) + .filter(agePolicy -> agePolicy.canBeApplied(age)) + .findFirst() + .orElseThrow(() -> new InvalidPolicyException("적용할 수 있는 정책이 존재하지 않습니다.")); + } + private boolean canBeApplied(final int age) { return validTarget.apply(age); } @@ -26,11 +33,4 @@ private boolean canBeApplied(final int age) { public double calculateDiscountFare(final double fare) { return discount.apply(fare); } - - public static AgePolicy search(final int age) { - return Arrays.stream(AgePolicy.values()) - .filter(agePolicy -> agePolicy.canBeApplied(age)) - .findFirst() - .orElseThrow(() -> new InvalidPolicyException("적용할 수 있는 정책이 존재하지 않습니다.")); - } } diff --git a/src/main/java/subway/domain/subway/LineWeightedEdge.java b/src/main/java/subway/domain/subway/LineWeightedEdge.java index 201c5a38d..8187f0852 100644 --- a/src/main/java/subway/domain/subway/LineWeightedEdge.java +++ b/src/main/java/subway/domain/subway/LineWeightedEdge.java @@ -32,14 +32,14 @@ public Long getLineId() { return lineId; } - public int getFareOfLine() { - return fareOfLine; - } - public void setLineId(final Long lineId) { this.lineId = lineId; } + public int getFareOfLine() { + return fareOfLine; + } + public void setFareOfLine(final int fareOfLine) { this.fareOfLine = fareOfLine; } From b49b926b743118f5a17170770f2a633b513aab16 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 01:37:14 +0900 Subject: [PATCH 29/40] =?UTF-8?q?refactor:=20=EC=B2=AD=EC=86=8C=EB=85=84,?= =?UTF-8?q?=20=EC=96=B4=EB=A6=B0=EC=9D=B4=EC=9D=B8=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=EC=97=90=EB=A7=8C=20=EA=B3=B5=EC=A0=9C=EA=B0=80=20=EC=9D=B4?= =?UTF-8?q?=EB=A3=A8=EC=96=B4=EC=A7=80=EB=8F=84=EB=A1=9D=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/domain/fare/AgeFareStrategy.java | 5 +---- .../java/subway/domain/fare/AgePolicy.java | 14 ++++++++++---- .../domain/fare/AgeFareStrategyTest.java | 19 ++++++++----------- .../subway/domain/fare/AgePolicyTest.java | 18 +++++++++--------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/main/java/subway/domain/fare/AgeFareStrategy.java b/src/main/java/subway/domain/fare/AgeFareStrategy.java index cff8250ec..2589e8c7c 100644 --- a/src/main/java/subway/domain/fare/AgeFareStrategy.java +++ b/src/main/java/subway/domain/fare/AgeFareStrategy.java @@ -5,12 +5,9 @@ public class AgeFareStrategy implements FareStrategy { - private static final int BASE_DEDUCTION = 350; - @Override public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { final AgePolicy agePolicy = AgePolicy.search(passenger.getAge()); - final double fareAfterDeduction = Math.max(0, fare - BASE_DEDUCTION); - return Math.max(0, fareAfterDeduction - agePolicy.calculateDiscountFare(fareAfterDeduction)); + return agePolicy.calculateDiscountFare(fare); } } diff --git a/src/main/java/subway/domain/fare/AgePolicy.java b/src/main/java/subway/domain/fare/AgePolicy.java index 48aaecc29..33e02de78 100644 --- a/src/main/java/subway/domain/fare/AgePolicy.java +++ b/src/main/java/subway/domain/fare/AgePolicy.java @@ -6,10 +6,16 @@ public enum AgePolicy { - BABY(age -> age < 6, fare -> 0d), - KID(age -> 6 <= age && age < 13, fare -> fare * 0.5), - TEEN(age -> 13 <= age && age < 19, fare -> fare * 0.2), - ADULT(age -> age >= 19, fare -> 0d); + BABY(age -> age < 6, fare -> fare), + KID(age -> 6 <= age && age < 13, fare -> { + final double deductionFare = Math.max(0, fare - 350); + return Math.max(0, deductionFare - deductionFare * 0.5); + }), + TEEN(age -> 13 <= age && age < 19, fare -> { + final double deductionFare = Math.max(0, fare - 350); + return Math.max(0, deductionFare - deductionFare * 0.2); + }), + ADULT(age -> age >= 19, fare -> fare); private final Function validTarget; private final Function discount; diff --git a/src/test/java/subway/domain/fare/AgeFareStrategyTest.java b/src/test/java/subway/domain/fare/AgeFareStrategyTest.java index 6df92d864..a3fa504b2 100644 --- a/src/test/java/subway/domain/fare/AgeFareStrategyTest.java +++ b/src/test/java/subway/domain/fare/AgeFareStrategyTest.java @@ -7,8 +7,6 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import subway.domain.line.Line; import subway.domain.subway.Passenger; import subway.domain.subway.Subway; @@ -18,16 +16,15 @@ class AgeFareStrategyTest { private static final AgeFareStrategy ageFareStrategy = new AgeFareStrategy(); - @ParameterizedTest - @ValueSource(ints = {-500, 0}) - @DisplayName("이전 요금이 0보다 작거나 같으면 요금은 없다.") - void zero(final int fare) { + @Test + @DisplayName("이전 요금이 0이라면 요금은 없다.") + void zero() { final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); lineOfTwo.addSection(GANGNAM, YANGJAE, 8); final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); final Passenger passenger = new Passenger(5, GANGNAM, YANGJAE); - final double result = ageFareStrategy.calculateFare(fare, passenger, subway); + final double result = ageFareStrategy.calculateFare(0, passenger, subway); assertThat(result).isEqualTo(0); } @@ -42,11 +39,11 @@ void baby() { final double result = ageFareStrategy.calculateFare(1350, passenger, subway); - assertThat(result).isEqualTo(1000); + assertThat(result).isEqualTo(1350); } @Test - @DisplayName("6세 이상, 13세 미만의 어린이인 경우 50% 할인이 적용된다.") + @DisplayName("6세 이상, 13세 미만의 어린이인 경우 350원 공제 후, 50% 할인이 적용된다.") void kid() { final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); lineOfTwo.addSection(GANGNAM, YANGJAE, 8); @@ -59,7 +56,7 @@ void kid() { } @Test - @DisplayName("13세 이상, 19세 미만 청소년인 경우 20% 할인이 적용된다.") + @DisplayName("13세 이상, 19세 미만 청소년인 경우 350원 공제 후, 20% 할인이 적용된다.") void teen() { final Line lineOfTwo = new Line(2L, "2호선", "초록색", 0); lineOfTwo.addSection(GANGNAM, YANGJAE, 8); @@ -81,6 +78,6 @@ void adult() { final double result = ageFareStrategy.calculateFare(1350, passenger, subway); - assertThat(result).isEqualTo(1000); + assertThat(result).isEqualTo(1350); } } diff --git a/src/test/java/subway/domain/fare/AgePolicyTest.java b/src/test/java/subway/domain/fare/AgePolicyTest.java index c9604825a..53b4dc71a 100644 --- a/src/test/java/subway/domain/fare/AgePolicyTest.java +++ b/src/test/java/subway/domain/fare/AgePolicyTest.java @@ -24,43 +24,43 @@ void search(final int age, final AgePolicy expected) { class CalculateDiscountFare { @Test - @DisplayName("BABY는 할인 금액이 0원이다.") + @DisplayName("BABY는 할인되지 않는다.") void baby() { final AgePolicy agePolicy = AgePolicy.BABY; final double result = agePolicy.calculateDiscountFare(1000); - assertThat(result).isEqualTo(0); + assertThat(result).isEqualTo(1000); } @Test - @DisplayName("KID는 50% 할인 금액을 반환한다.") + @DisplayName("KID는 350원 공제 후, 50% 할인된 금액을 반환한다.") void kid() { final AgePolicy agePolicy = AgePolicy.KID; - final double result = agePolicy.calculateDiscountFare(1000); + final double result = agePolicy.calculateDiscountFare(1350); assertThat(result).isEqualTo(500); } @Test - @DisplayName("TEEN은 20% 할인 금액을 반환한다.") + @DisplayName("TEEN은 350원 공제 후, 20% 할인된 금액을 반환한다.") void teen() { final AgePolicy agePolicy = AgePolicy.TEEN; - final double result = agePolicy.calculateDiscountFare(1000); + final double result = agePolicy.calculateDiscountFare(1350); - assertThat(result).isEqualTo(200); + assertThat(result).isEqualTo(800); } @Test - @DisplayName("ADULT는 할인 금액이 0원이다.") + @DisplayName("ADULT는 할인되지 않는다.") void adult() { final AgePolicy agePolicy = AgePolicy.ADULT; final double result = agePolicy.calculateDiscountFare(1000); - assertThat(result).isEqualTo(0); + assertThat(result).isEqualTo(1000); } } } From ea0d0cf1313bc540dc66617b4ab2c3bd1b8dff06 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 01:44:45 +0900 Subject: [PATCH 30/40] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/domain/section/PathSection.java | 13 +++++++------ src/main/java/subway/entity/LineEntity.java | 8 ++++---- src/main/java/subway/service/SubwayService.java | 3 ++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/subway/domain/section/PathSection.java b/src/main/java/subway/domain/section/PathSection.java index bef4d993a..1f8aced18 100644 --- a/src/main/java/subway/domain/section/PathSection.java +++ b/src/main/java/subway/domain/section/PathSection.java @@ -1,5 +1,6 @@ package subway.domain.section; +import subway.domain.line.Fare; import subway.domain.station.Station; import subway.domain.subway.LineWeightedEdge; @@ -8,8 +9,8 @@ public class PathSection { private final Long lineId; private final Station source; private final Station target; - private final int distance; - private final int fareOfLine; + private final Distance distance; + private final Fare fareOfLine; public PathSection( final Long lineId, @@ -21,8 +22,8 @@ public PathSection( this.lineId = lineId; this.source = source; this.target = target; - this.distance = distance; - this.fareOfLine = fareOfLine; + this.distance = new Distance(distance); + this.fareOfLine = new Fare(fareOfLine); } public static PathSection from(final LineWeightedEdge edge) { @@ -48,10 +49,10 @@ public Station getTarget() { } public int getDistance() { - return distance; + return distance.getValue(); } public int getFareOfLine() { - return fareOfLine; + return fareOfLine.getValue(); } } diff --git a/src/main/java/subway/entity/LineEntity.java b/src/main/java/subway/entity/LineEntity.java index 8e3769b3f..fc4a15df4 100644 --- a/src/main/java/subway/entity/LineEntity.java +++ b/src/main/java/subway/entity/LineEntity.java @@ -8,13 +8,13 @@ public class LineEntity { private final Long id; private final String name; private final String color; - private final int fare; + private final Integer fare; - public LineEntity(final String name, final String color, final int fare) { + public LineEntity(final String name, final String color, final Integer fare) { this(null, name, color, fare); } - public LineEntity(final Long id, final String name, final String color, final int fare) { + public LineEntity(final Long id, final String name, final String color, final Integer fare) { this.id = id; this.name = name; this.color = color; @@ -37,7 +37,7 @@ public String getColor() { return color; } - public int getFare() { + public Integer getFare() { return fare; } diff --git a/src/main/java/subway/service/SubwayService.java b/src/main/java/subway/service/SubwayService.java index 01350112f..7337ff9b3 100644 --- a/src/main/java/subway/service/SubwayService.java +++ b/src/main/java/subway/service/SubwayService.java @@ -16,8 +16,8 @@ import subway.repository.LineRepository; import subway.repository.StationRepository; -@Transactional(readOnly = true) @Service +@Transactional(readOnly = true) public class SubwayService { private final LineRepository lineRepository; @@ -58,3 +58,4 @@ private Passenger generatePassenger(final PassengerRequest request) { return new Passenger(request.getAge(), start, end); } } + From 6f45d1720c932dfc731b5f1a6937a98f959e61bd Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 02:36:05 +0900 Subject: [PATCH 31/40] =?UTF-8?q?refactor:=20=EB=85=B8=EC=84=A0=EC=97=90?= =?UTF-8?q?=20=EC=97=AD=20=EC=B6=94=EA=B0=80=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/subway/domain/line/Line.java | 66 ++++++++++++---------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/src/main/java/subway/domain/line/Line.java b/src/main/java/subway/domain/line/Line.java index d768479e3..1095073fc 100644 --- a/src/main/java/subway/domain/line/Line.java +++ b/src/main/java/subway/domain/line/Line.java @@ -37,7 +37,8 @@ public Line(final Long id, final String name, final String color, final int fare public void addSection(final Station upward, final Station downward, final int distance) { if (sections.isEmpty()) { - addInitialSection(upward, downward, distance); + sections.add(new Section(upward, downward, distance)); + sections.add(new Section(downward, Station.TERMINAL, 0)); return; } @@ -45,25 +46,21 @@ public void addSection(final Station upward, final Station downward, final int d final int downwardPosition = sections.findPosition(downward); validateForAddSection(upwardPosition, downwardPosition); - if (shouldAdd(upwardPosition)) { - if (isFirstSection(downwardPosition)) { - sections.add(0, new Section(upward, downward, distance)); + if (upwardPosition == ADDITIONAL_INDEX) { + if (isAddAtFront(downwardPosition)) { + addSectionEndPoints(true, upward, downward, distance); return; } addUpwardSectionBetweenStations(upward, downward, distance, downwardPosition); - return; } - if (isLastSection(upwardPosition)) { - addDownwardSectionInLast(upward, downward, distance); - return; + if (downwardPosition == ADDITIONAL_INDEX) { + if (isAddAtEnd(upwardPosition)) { + addSectionEndPoints(false, upward, downward, distance); + return; + } + addDownwardSectionBetweenStations(upward, downward, distance, upwardPosition); } - addDownwardSectionBetweenStations(upward, downward, distance, upwardPosition); - } - - private void addInitialSection(final Station upward, final Station downward, final int distance) { - sections.add(new Section(upward, downward, distance)); - sections.add(new Section(downward, Station.TERMINAL, 0)); } private void validateForAddSection(final int upwardPosition, final int downwardPosition) { @@ -75,21 +72,40 @@ private void validateForAddSection(final int upwardPosition, final int downwardP } } - private boolean shouldAdd(final int position) { - return position == ADDITIONAL_INDEX; + private boolean isAddAtFront(final int downwardPosition) { + return downwardPosition == 0; } - private boolean isFirstSection(final int position) { - return position == 0; + private boolean isAddAtEnd(final int upwardPosition) { + return upwardPosition == sections.size() - 1; + } + + private void addSectionEndPoints( + final boolean isFirst, + final Station upward, + final Station downward, + final int distance + ) { + sections.deleteByPosition(sections.size() - 1); + sections.add(getEndPosition(isFirst), new Section(upward, downward, distance)); + final Section lastSection = sections.findSectionByPosition(sections.size() - 1); + sections.add(sections.size(), new Section(lastSection.getDownward(), Station.TERMINAL, 0)); + } + + public int getEndPosition(final boolean isFirst) { + if (isFirst) { + return 0; + } + return sections.size(); } private void addUpwardSectionBetweenStations( final Station upward, final Station downward, final int distance, - final int downwardPosition + final int position ) { - final int targetPosition = downwardPosition - 1; + final int targetPosition = position - 1; final Section section = sections.findSectionByPosition(targetPosition); sections.deleteByPosition(targetPosition); validateDistance(section.getDistance(), distance); @@ -97,16 +113,6 @@ private void addUpwardSectionBetweenStations( sections.add(targetPosition, new Section(section.getUpward(), upward, section.getDistance() - distance)); } - private boolean isLastSection(final int position) { - return sections.size() - 1 == position; - } - - private void addDownwardSectionInLast(final Station upward, final Station downward, final int distance) { - sections.deleteByPosition(sections.size() - 1); - sections.add(sections.size(), new Section(upward, downward, distance)); - sections.add(sections.size(), new Section(downward, Station.TERMINAL, 0)); - } - private void addDownwardSectionBetweenStations( final Station upward, final Station downward, From ca69a6a90c7bcca218cb0dfbb3e518b64974912b Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 04:42:29 +0900 Subject: [PATCH 32/40] =?UTF-8?q?fix:=20=EB=8B=A4=EC=9D=8C=20=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=EB=B2=88=ED=98=B8=EB=A1=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/controller/dto/response/ShortestPathResponse.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java index 66bdf07e6..c7c498163 100644 --- a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java +++ b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java @@ -42,13 +42,14 @@ public static ShortestPathResponse of( private static List generateLineSections(final List sections) { final List result = new ArrayList<>(); - final Long currentLineId = sections.get(0).getLineId(); - + Long currentLineId = sections.get(0).getLineId(); List currentSections = new ArrayList<>(); + for (final PathSection section : sections) { if (section.getLineId() != currentLineId) { result.add(LineSectionResponse.from(currentSections)); currentSections = new ArrayList<>(); + currentLineId = section.getLineId(); } currentSections.add(section); } From b3461b843409bc3fd50be91da3beac77188cd5d7 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 04:43:12 +0900 Subject: [PATCH 33/40] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EB=8D=95?= =?UTF-8?q?=EC=85=98=EA=B3=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + docker/.env | 4 +++ docker/docker-compose.yml | 37 +++++++++++++++++++++++++ docker/mysql.cnf | 44 ++++++++++++++++++++++++++++++ src/main/resources/application.yml | 6 ++-- src/main/resources/data.sql | 40 +++++++++++++-------------- 6 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 docker/.env create mode 100644 docker/docker-compose.yml create mode 100644 docker/mysql.cnf diff --git a/build.gradle b/build.gradle index 97fb52170..c4c067cab 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ dependencies { implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' implementation 'org.jgrapht:jgrapht-core:1.5.2' + implementation 'mysql:mysql-connector-java:8.0.33' testImplementation 'io.rest-assured:rest-assured:4.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/docker/.env b/docker/.env new file mode 100644 index 000000000..534d3fe10 --- /dev/null +++ b/docker/.env @@ -0,0 +1,4 @@ +MYSQL_DATABASE=subway +MYSQL_ROOT_PASSWORD=1234 +MYSQL_USER=subway +MYSQL_PASSWORD=1234 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..4ffcc96bb --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,37 @@ +version: "3" +services: + app: + container_name: app + image: arm64v8/amazoncorretto:11-alpine-jdk + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/${MYSQL_DATABASE}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} + SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - ../src:/app/src + - ../gradle:/app/gradle + - ../build.gradle:/app/build.gradle + - ../gradlew:/app/gradlew + - ../gradlew.bat:/app/gradlew.bat + working_dir: /app + command: [ "./gradlew", "bootrun" ] + depends_on: + - db + restart: always + + db: + container_name: db + image: mysql:8.0.33 + ports: + - "3307:3306" + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - ../src/main/resources/data.sql:/docker-entrypoint-initdb.d/init.sql + - ./mysql.cnf:/etc/mysql/conf.d/mysql.cnf + restart: always diff --git a/docker/mysql.cnf b/docker/mysql.cnf new file mode 100644 index 000000000..ea91bbc28 --- /dev/null +++ b/docker/mysql.cnf @@ -0,0 +1,44 @@ +# Copyright (c) 2015, 2021, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, +# as published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an additional +# permission to link the program and your derivative works with the +# separately licensed software that they have included with MySQL. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# +# The MySQL Client configuration file. +# +# For explanations see +# http://dev.mysql.com/doc/mysql/en/server-system-variables.html + +[client] +default-character-set=utf8mb4 + +[mysqld] +character-set-client-handshake=FALSE +init_connect="SET collation_connection=utf8mb4_unicode_ci" +init_connect="SET NAMES utf8mb4" +character-set-server=utf8mb4 +default_time_zone=Asia/Seoul +lower_case_table_names=1 + +[mysql] +default-character-set=utf8mb4 + +[mysqldump] +default-character-set=utf8mb4 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 70bb8d36b..c4f505fe4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,8 +4,10 @@ spring: enabled: true datasource: - url: jdbc:h2:mem:testdb;MODE=MySQL - driver-class-name: org.h2.Driver + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} sql: init: diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index f0b85f685..cfabf10c3 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,28 +1,28 @@ -CREATE TABLE IF NOT EXISTS STATION +CREATE TABLE IF NOT EXISTS station ( - id BIGINT AUTO_INCREMENT NOT NULL, - name VARCHAR(15) NOT NULL, - PRIMARY KEY (id) + `id` BIGINT AUTO_INCREMENT NOT NULL, + `name` VARCHAR(15) NOT NULL, + PRIMARY KEY (`id`) ); -CREATE TABLE IF NOT EXISTS LINE +CREATE TABLE IF NOT EXISTS line ( - id BIGINT AUTO_INCREMENT NOT NULL, - name VARCHAR(15) NOT NULL, - color VARCHAR(15) NOT NULL, - fare INT NOT NULL, - PRIMARY KEY (id) + `id` BIGINT AUTO_INCREMENT NOT NULL, + `name` VARCHAR(15) NOT NULL, + `color` VARCHAR(15) NOT NULL, + `fare` INT NOT NULL, + PRIMARY KEY (`id`) ); -CREATE TABLE IF NOT EXISTS SECTION +CREATE TABLE IF NOT EXISTS section ( - id BIGINT AUTO_INCREMENT NOT NULL, - line_id BIGINT NOT NULL, - upward_station_id BIGINT NOT NULL, - downward_station_id BIGINT NOT NULL, - distance INT NOT NULL, - PRIMARY KEY (id), - FOREIGN KEY (line_id) REFERENCES LINE (id), - FOREIGN KEY (upward_station_id) REFERENCES STATION (id), - FOREIGN KEY (downward_station_id) REFERENCES STATION (id) + `id` BIGINT AUTO_INCREMENT NOT NULL, + `line_id` BIGINT NOT NULL, + `upward_station_id` BIGINT NOT NULL, + `downward_station_id` BIGINT NOT NULL, + `distance` INT NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`line_id`) REFERENCES `line` (`id`), + FOREIGN KEY (`upward_station_id`) REFERENCES `station` (`id`), + FOREIGN KEY (`downward_station_id`) REFERENCES `station` (`id`) ); From 469118da73cf4e60929bc9e9a41ecd8764b72757 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Thu, 18 May 2023 09:45:50 +0900 Subject: [PATCH 34/40] =?UTF-8?q?docs:=20=EC=95=A0=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=EB=B9=A0=EB=A5=B8=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d58dd1577..199459ae0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # jwp-subway-path +## Quick Start + +```text +cd docker + +docker-compose -p subway up +``` + ## API 문서 ```text From ac9382c26a632b7d597decc5b447c88f6d93bd5a Mon Sep 17 00:00:00 2001 From: woo-chang Date: Fri, 19 May 2023 15:24:55 +0900 Subject: [PATCH 35/40] =?UTF-8?q?refactor:=20=ED=96=89=EC=9C=84=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20=EC=9D=98=EB=AF=B8=20=EB=B6=80=EC=97=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/domain/fare/DistancePolicy.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/subway/domain/fare/DistancePolicy.java b/src/main/java/subway/domain/fare/DistancePolicy.java index 96bc4cdea..215eecf75 100644 --- a/src/main/java/subway/domain/fare/DistancePolicy.java +++ b/src/main/java/subway/domain/fare/DistancePolicy.java @@ -6,17 +6,11 @@ public enum DistancePolicy { BASE_FIFTY(50, distance -> { final long additionDistance = distance - 50; - if (additionDistance <= 0) { - return 0L; - } - return (long) ((Math.ceil((additionDistance - 1) / 8) + 1) * 100); + return calculateDistanceFarePerStep(additionDistance, 8); }), BASE_TEN(10, distance -> { final long additionDistance = Math.min(BASE_FIFTY.base, distance) - 10; - if (additionDistance <= 0) { - return 0L; - } - return (long) ((Math.ceil((additionDistance - 1) / 5) + 1) * 100); + return calculateDistanceFarePerStep(additionDistance, 5); }); private final int base; @@ -27,6 +21,13 @@ public enum DistancePolicy { this.policy = policy; } + private static long calculateDistanceFarePerStep(final long distance, final int step) { + if (distance <= 0) { + return 0L; + } + return (long) ((Math.ceil((distance - 1) / step) + 1) * 100); + } + public long calculateAdditionFare(final long distance) { return policy.apply(distance); } From bbfecb69b1b8bce5cdae332345791a00b9db2323 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Sat, 20 May 2023 16:51:25 +0900 Subject: [PATCH 36/40] =?UTF-8?q?refactor:=20=EC=9A=94=EA=B8=88=20?= =?UTF-8?q?=EC=A0=95=EC=B1=85=20=EC=83=9D=EC=84=B1=EC=9D=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=95=88=EC=97=90=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/fare/FareStrategyCompositeTest.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java b/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java index cc00b9716..bc2e7282f 100644 --- a/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java +++ b/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java @@ -14,17 +14,16 @@ class FareStrategyCompositeTest { - private static final FareStrategyComposite fareStrategyComposite = new FareStrategyComposite( - List.of( - new DistanceFareStrategy(), - new RouteFareStrategy(), - new AgeFareStrategy() - ) - ); - @Test @DisplayName("지정된 요금 정책을 수행 후 요금을 확인한다.") void calculateFare() { + final FareStrategyComposite fareStrategyComposite = new FareStrategyComposite( + List.of( + new DistanceFareStrategy(), + new RouteFareStrategy(), + new AgeFareStrategy() + ) + ); final Line lineOfTwo = new Line(2L, "2호선", "초록색", 1000); lineOfTwo.addSection(GANGNAM, YANGJAE, 12); final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); From 084d6c05f377d3465858658fc2d8a4f823ebe98e Mon Sep 17 00:00:00 2001 From: woo-chang Date: Sat, 20 May 2023 17:12:09 +0900 Subject: [PATCH 37/40] =?UTF-8?q?refactor:=20dto=20swagger=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/controller/dto/request/LineCreateRequest.java | 4 ++++ .../java/subway/controller/dto/request/PassengerRequest.java | 4 ++++ .../subway/controller/dto/request/SectionCreateRequest.java | 4 ++++ .../subway/controller/dto/request/StationCreateRequest.java | 4 ++++ .../java/subway/controller/dto/response/LineResponse.java | 4 ++++ .../subway/controller/dto/response/LineSectionResponse.java | 4 ++++ .../java/subway/controller/dto/response/LinesResponse.java | 4 ++++ .../java/subway/controller/dto/response/SectionResponse.java | 4 ++++ .../subway/controller/dto/response/ShortestPathResponse.java | 4 ++++ .../java/subway/controller/dto/response/StationResponse.java | 4 ++++ 10 files changed, 40 insertions(+) diff --git a/src/main/java/subway/controller/dto/request/LineCreateRequest.java b/src/main/java/subway/controller/dto/request/LineCreateRequest.java index e30ccf7fe..5ce7726e2 100644 --- a/src/main/java/subway/controller/dto/request/LineCreateRequest.java +++ b/src/main/java/subway/controller/dto/request/LineCreateRequest.java @@ -5,6 +5,10 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +@Schema( + description = "노선 생성 요청 정보", + example = "{\"name\": \"2호선\", \"color\": \"초록색\", \"fare\": 1000}" +) public class LineCreateRequest { @Schema(description = "노선 이름") diff --git a/src/main/java/subway/controller/dto/request/PassengerRequest.java b/src/main/java/subway/controller/dto/request/PassengerRequest.java index f5f4bebaf..daeb55195 100644 --- a/src/main/java/subway/controller/dto/request/PassengerRequest.java +++ b/src/main/java/subway/controller/dto/request/PassengerRequest.java @@ -4,6 +4,10 @@ import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; +@Schema( + description = "지하철 승객 정보", + example = "{\"age\": 15, \"startStationId\": 1, \"endStationId\": 2}" +) public class PassengerRequest { @Schema(description = "탑승자 나이") diff --git a/src/main/java/subway/controller/dto/request/SectionCreateRequest.java b/src/main/java/subway/controller/dto/request/SectionCreateRequest.java index db27cce27..0bb854ef7 100644 --- a/src/main/java/subway/controller/dto/request/SectionCreateRequest.java +++ b/src/main/java/subway/controller/dto/request/SectionCreateRequest.java @@ -4,6 +4,10 @@ import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; +@Schema( + description = "구간 정보 생성 요청 정보", + example = "{\"upwardStationId\": 1, \"downwardStationId\": 2, \"distance\": 5}" +) public class SectionCreateRequest { @Schema(description = "상행 역 ID") diff --git a/src/main/java/subway/controller/dto/request/StationCreateRequest.java b/src/main/java/subway/controller/dto/request/StationCreateRequest.java index 9cfca2943..07fc5e6f7 100644 --- a/src/main/java/subway/controller/dto/request/StationCreateRequest.java +++ b/src/main/java/subway/controller/dto/request/StationCreateRequest.java @@ -3,6 +3,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import javax.validation.constraints.NotBlank; +@Schema( + description = "역 생성 요청 정보", + example = "{\"name\": \"잠실역\"}" +) public class StationCreateRequest { @Schema(description = "역 이름") diff --git a/src/main/java/subway/controller/dto/response/LineResponse.java b/src/main/java/subway/controller/dto/response/LineResponse.java index fe5dd23af..0439caf4a 100644 --- a/src/main/java/subway/controller/dto/response/LineResponse.java +++ b/src/main/java/subway/controller/dto/response/LineResponse.java @@ -6,6 +6,10 @@ import subway.domain.line.Line; import subway.domain.station.Station; +@Schema( + description = "역 응답 정보", + example = "{\"id\": 1, \"name\": \"2호선\", \"color\": \"초록색\", \"fare\": 1000, \"stations\": [{\"id\": 1, \"name\": \"잠실역\"}]}" +) public class LineResponse { @Schema(description = "노선 ID") diff --git a/src/main/java/subway/controller/dto/response/LineSectionResponse.java b/src/main/java/subway/controller/dto/response/LineSectionResponse.java index 546ce99ea..24e4cb148 100644 --- a/src/main/java/subway/controller/dto/response/LineSectionResponse.java +++ b/src/main/java/subway/controller/dto/response/LineSectionResponse.java @@ -5,6 +5,10 @@ import java.util.List; import subway.domain.section.PathSection; +@Schema( + description = "구간 응답 정보", + example = "{\"lineId\": 1, \"sections\": [{\"upwardStationName\": \"잠실역\", \"downwardStationName\": \"잠실새내역\", \"distance\": 5}]}" +) public class LineSectionResponse { @Schema(description = "노선 ID") diff --git a/src/main/java/subway/controller/dto/response/LinesResponse.java b/src/main/java/subway/controller/dto/response/LinesResponse.java index 1ad249d82..b58c6c78b 100644 --- a/src/main/java/subway/controller/dto/response/LinesResponse.java +++ b/src/main/java/subway/controller/dto/response/LinesResponse.java @@ -3,6 +3,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; +@Schema( + description = "노선 목록 응답 정보", + example = "{\"lines\": [{\"id\": 1, \"name\": \"2호선\", \"color\": \"초록색\", \"fare\": 1000, \"stations\": [{\"id\": 1, \"name\": \"잠실역\"}]}]}" +) public class LinesResponse { @Schema(description = "노선 목록") diff --git a/src/main/java/subway/controller/dto/response/SectionResponse.java b/src/main/java/subway/controller/dto/response/SectionResponse.java index af3093195..40eae5e29 100644 --- a/src/main/java/subway/controller/dto/response/SectionResponse.java +++ b/src/main/java/subway/controller/dto/response/SectionResponse.java @@ -3,6 +3,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import subway.domain.section.PathSection; +@Schema( + description = "구간 응답 정보", + example = "{\"upwardStationName\": \"잠실역\", \"downwardStationName\": \"잠실새내역\", \"distance\": 10}" +) public class SectionResponse { @Schema(description = "상행역 이름") diff --git a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java index c7c498163..46e49e32b 100644 --- a/src/main/java/subway/controller/dto/response/ShortestPathResponse.java +++ b/src/main/java/subway/controller/dto/response/ShortestPathResponse.java @@ -5,6 +5,10 @@ import java.util.List; import subway.domain.section.PathSection; +@Schema( + description = "최단 경로 응답 정보", + example = "{\"transferCount\": 3, \"path\": [{\"lineId\": 1, \"sections\": [{\"upwardStationName\": \"잠실역\", \"downwardStationName\": \"잠실새내역\", \"distance\": 5}]}], \"totalDistance\": 10, \"subwayFare\": 2000}" +) public class ShortestPathResponse { @Schema(description = "환승 횟수") diff --git a/src/main/java/subway/controller/dto/response/StationResponse.java b/src/main/java/subway/controller/dto/response/StationResponse.java index 3175c95f5..1ffeb10b4 100644 --- a/src/main/java/subway/controller/dto/response/StationResponse.java +++ b/src/main/java/subway/controller/dto/response/StationResponse.java @@ -3,6 +3,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import subway.domain.station.Station; +@Schema( + description = "역 응답 정보", + example = "{\"id\": 1, \"name\": \"잠실역\"}" +) public class StationResponse { @Schema(description = "역 ID") From 50bfbb3754a4f9cecc273a85898fc4b14dec728c Mon Sep 17 00:00:00 2001 From: woo-chang Date: Sat, 20 May 2023 18:00:01 +0900 Subject: [PATCH 38/40] =?UTF-8?q?test:=20=EA=B5=AC=EA=B0=84=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/section/SectionsTest.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/test/java/subway/domain/section/SectionsTest.java diff --git a/src/test/java/subway/domain/section/SectionsTest.java b/src/test/java/subway/domain/section/SectionsTest.java new file mode 100644 index 000000000..cd5daa5cf --- /dev/null +++ b/src/test/java/subway/domain/section/SectionsTest.java @@ -0,0 +1,155 @@ +package subway.domain.section; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import subway.domain.station.Station; + +class SectionsTest { + + @Test + @DisplayName("새로운 구간을 추가한다.") + void add() { + final Sections sections = new Sections(new ArrayList<>()); + final Section section = new Section(new Station("잠실역"), new Station("잠실새내역"), 10); + + sections.add(section); + + assertThat(sections.size()).isEqualTo(1); + } + + @Test + @DisplayName("특정 위치에 새로운 구간을 추가한다.") + void addAtPosition() { + final Sections sections = new Sections(new ArrayList<>()); + final Section oldSection = new Section(new Station("잠실역"), new Station("잠실새내역"), 10); + sections.add(oldSection); + final Section newSection = new Section(new Station("강남역"), new Station("잠실역"), 10); + + sections.add(0, newSection); + + assertAll( + () -> assertThat(sections.size()).isEqualTo(2), + () -> assertThat(sections.findSectionByPosition(0)).isEqualTo(newSection), + () -> assertThat(sections.findSectionByPosition(1)).isEqualTo(oldSection) + ); + } + + @Test + @DisplayName("특정 위치의 구간을 조회한다.") + void findSectionByPosition() { + final Sections sections = new Sections(new ArrayList<>()); + final Section section = new Section(new Station("잠실역"), new Station("잠실새내역"), 10); + sections.add(0, section); + + final Section findSection = sections.findSectionByPosition(0); + + assertThat(findSection).isEqualTo(section); + } + + @Test + @DisplayName("특정 위치의 구간을 삭제한다.") + void deleteByPosition() { + final Sections sections = new Sections(new ArrayList<>()); + final Section section = new Section(new Station("잠실역"), new Station("잠실새내역"), 10); + sections.add(0, section); + + sections.deleteByPosition(0); + + assertThat(sections.size()).isEqualTo(0); + } + + @Nested + @DisplayName("findPosition 메서드는 ") + class FindPosition { + + @Test + @DisplayName("역이 상행역으로 존재하는 구간 위치를 찾는다.") + void findPosition() { + final Sections sections = new Sections(new ArrayList<>()); + final Station upward = new Station(1L, "잠실역"); + final Station downward = new Station(2L, "잠실새내역"); + final Section section = new Section(upward, downward, 10); + sections.add(0, section); + + final int position = sections.findPosition(upward); + + assertThat(position).isEqualTo(0); + } + + @Test + @DisplayName("역이 상행역으로 존재하는 구간이 존재하지 않으면 -1을 반환한다.") + void findPositionWithNotExistStation() { + final Sections sections = new Sections(new ArrayList<>()); + final Station upward = new Station(1L, "잠실역"); + final Station downward = new Station(2L, "잠실새내역"); + final Section section = new Section(upward, downward, 10); + sections.add(0, section); + + final int position = sections.findPosition(new Station(3L, "강남역")); + + assertThat(position).isEqualTo(-1); + } + } + + @Nested + @DisplayName("isEmpty 메서드는 ") + class IsEmpty { + + @Test + @DisplayName("구간 정보가 존재하지 않으면 true 반환한다.") + void isEmptyTrue() { + final Sections sections = new Sections(new ArrayList<>()); + + final boolean result = sections.isEmpty(); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("구간 정보가 존재하면 false 반환한다.") + void isEmptyFalse() { + final Sections sections = new Sections(new ArrayList<>()); + final Section section = new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10); + sections.add(section); + + final boolean result = sections.isEmpty(); + + assertThat(result).isFalse(); + } + } + + @Test + @DisplayName("모든 구간 목록 정보를 삭제한다.") + void clear() { + final Sections sections = new Sections(new ArrayList<>()); + final Section section = new Section(new Station(1L, "잠실역"), new Station(2L, "잠실새내역"), 10); + sections.add(section); + + sections.clear(); + + assertThat(sections.size()).isEqualTo(0); + } + + @Test + @DisplayName("상행 역 목록을 조회한다.") + void getUpwards() { + final Sections sections = new Sections(new ArrayList<>()); + final Station jamsil = new Station(1L, "잠실역"); + final Station jamsilsaenae = new Station(2L, "잠실새내역"); + final Station geondae = new Station(3L, "건대역"); + final Section firstSection = new Section(jamsil, jamsilsaenae, 10); + final Section secondSection = new Section(jamsilsaenae, geondae, 10); + sections.add(firstSection); + sections.add(secondSection); + + final List result = sections.getUpwards(); + + assertThat(result).isEqualTo(List.of(jamsil, jamsilsaenae)); + } +} From 8d88f7f904e8645a99954810250cbf38783a35d7 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Tue, 23 May 2023 00:05:11 +0900 Subject: [PATCH 39/40] =?UTF-8?q?refactor:=20FareStrategyComposite?= =?UTF-8?q?=EB=A5=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=8A=A4?= =?UTF-8?q?=EC=BA=94=EC=9C=BC=EB=A1=9C=20=EB=B9=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/subway/config/FareConfiguration.java | 25 ------------------- .../subway/domain/fare/AgeFareStrategy.java | 2 +- .../domain/fare/DistanceFareStrategy.java | 2 +- .../domain/fare/FareStrategyComposite.java | 12 +++++---- .../subway/domain/fare/RouteFareStrategy.java | 2 +- .../fare/FareStrategyCompositeTest.java | 8 +----- 6 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 src/main/java/subway/config/FareConfiguration.java diff --git a/src/main/java/subway/config/FareConfiguration.java b/src/main/java/subway/config/FareConfiguration.java deleted file mode 100644 index d3bc31713..000000000 --- a/src/main/java/subway/config/FareConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package subway.config; - -import java.util.List; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import subway.domain.fare.AgeFareStrategy; -import subway.domain.fare.DistanceFareStrategy; -import subway.domain.fare.FareStrategy; -import subway.domain.fare.FareStrategyComposite; -import subway.domain.fare.RouteFareStrategy; - -@Configuration -public class FareConfiguration { - - @Bean - public FareStrategy fareStrategy() { - return new FareStrategyComposite( - List.of( - new DistanceFareStrategy(), - new RouteFareStrategy(), - new AgeFareStrategy() - ) - ); - } -} diff --git a/src/main/java/subway/domain/fare/AgeFareStrategy.java b/src/main/java/subway/domain/fare/AgeFareStrategy.java index 2589e8c7c..0f016f67e 100644 --- a/src/main/java/subway/domain/fare/AgeFareStrategy.java +++ b/src/main/java/subway/domain/fare/AgeFareStrategy.java @@ -3,7 +3,7 @@ import subway.domain.subway.Passenger; import subway.domain.subway.Subway; -public class AgeFareStrategy implements FareStrategy { +class AgeFareStrategy implements FareStrategy { @Override public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { diff --git a/src/main/java/subway/domain/fare/DistanceFareStrategy.java b/src/main/java/subway/domain/fare/DistanceFareStrategy.java index ca5ffebd5..3a37e254e 100644 --- a/src/main/java/subway/domain/fare/DistanceFareStrategy.java +++ b/src/main/java/subway/domain/fare/DistanceFareStrategy.java @@ -3,7 +3,7 @@ import subway.domain.subway.Passenger; import subway.domain.subway.Subway; -public class DistanceFareStrategy implements FareStrategy { +class DistanceFareStrategy implements FareStrategy { private static final int DEFAULT_FARE = 1250; diff --git a/src/main/java/subway/domain/fare/FareStrategyComposite.java b/src/main/java/subway/domain/fare/FareStrategyComposite.java index 8877b3d8b..8c4d52c4d 100644 --- a/src/main/java/subway/domain/fare/FareStrategyComposite.java +++ b/src/main/java/subway/domain/fare/FareStrategyComposite.java @@ -1,16 +1,18 @@ package subway.domain.fare; import java.util.List; +import org.springframework.stereotype.Component; import subway.domain.subway.Passenger; import subway.domain.subway.Subway; +@Component public class FareStrategyComposite implements FareStrategy { - private final List strategies; - - public FareStrategyComposite(final List strategies) { - this.strategies = strategies; - } + private final List strategies = List.of( + new DistanceFareStrategy(), + new RouteFareStrategy(), + new AgeFareStrategy() + ); @Override public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { diff --git a/src/main/java/subway/domain/fare/RouteFareStrategy.java b/src/main/java/subway/domain/fare/RouteFareStrategy.java index c0d50b74f..b815a0409 100644 --- a/src/main/java/subway/domain/fare/RouteFareStrategy.java +++ b/src/main/java/subway/domain/fare/RouteFareStrategy.java @@ -5,7 +5,7 @@ import subway.domain.subway.Passenger; import subway.domain.subway.Subway; -public class RouteFareStrategy implements FareStrategy { +class RouteFareStrategy implements FareStrategy { @Override public double calculateFare(final double fare, final Passenger passenger, final Subway subway) { diff --git a/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java b/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java index bc2e7282f..00c0133ca 100644 --- a/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java +++ b/src/test/java/subway/domain/fare/FareStrategyCompositeTest.java @@ -17,13 +17,7 @@ class FareStrategyCompositeTest { @Test @DisplayName("지정된 요금 정책을 수행 후 요금을 확인한다.") void calculateFare() { - final FareStrategyComposite fareStrategyComposite = new FareStrategyComposite( - List.of( - new DistanceFareStrategy(), - new RouteFareStrategy(), - new AgeFareStrategy() - ) - ); + final FareStrategyComposite fareStrategyComposite = new FareStrategyComposite(); final Line lineOfTwo = new Line(2L, "2호선", "초록색", 1000); lineOfTwo.addSection(GANGNAM, YANGJAE, 12); final Subway subway = new Subway(new SubwayJgraphtGraph(List.of(lineOfTwo))); From 1d0e6c3f21acba500d32a358a1b32b2a57dcb9e0 Mon Sep 17 00:00:00 2001 From: woo-chang Date: Tue, 23 May 2023 00:29:59 +0900 Subject: [PATCH 40/40] =?UTF-8?q?refactor:=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=20=EB=94=B0=EB=A5=B8=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 4 ---- src/test/resources/application.yml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c4f505fe4..b3e5032b4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,8 +1,4 @@ spring: - h2: - console: - enabled: true - datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${SPRING_DATASOURCE_URL} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a07e737ec..273a4b14c 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,8 +1,4 @@ spring: - h2: - console: - enabled: true - datasource: url: jdbc:h2:mem:testdb;MODE=MySQL driver-class-name: org.h2.Driver