From 944f9c4c0d5816df902857694f379c58c55a4093 Mon Sep 17 00:00:00 2001 From: teo <78679830+woosung1223@users.noreply.github.com> Date: Tue, 23 May 2023 00:29:00 +0900 Subject: [PATCH] =?UTF-8?q?[2=EB=8B=A8=EA=B3=84=20-=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5]=20=ED=85=8C?= =?UTF-8?q?=EC=98=A4(=EC=B5=9C=EC=9A=B0=EC=84=B1)=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 테이블 이름 변경 * refactor: 외래키 제약조건 추가 * refactor: 데이터베이스 예외 발생 시 문구 반환 * chore: 코드 리포맷팅 * refactor: 동등성 비교조건 변경 * refactor: URI에서 컬렉션이 드러나도록 변경 * test: 컨트롤러 테스트 보완 * refactor: logback-access가 아닌 인터셉터로 로깅 처리 * refactor: 서비스 입력 모델 생성 * refactor: 서비스 출력 모델 생성 * chore: 패키지 구조 변경 * chore: 패키지 이름 변경 * feat: 경로 조회 및 요금 계산 비즈니스 로직 구현 * refactor: RouteMap 객체를 재사용 가능하게 Section List으로 구성 * feat: 요금 계산용 값 객체 생성 * refactor: 경로 조회 및 요금 계산 로직 수정 * feat: 경로탐색 구현체 생성 * feat: 경로 조회 및 요금 계산 API 구현 * test: 통합 테스트용 http 파일 생성 * chore: 테스트 패키지 구조 프로덕션과 동일하게 변경 * fix: 몇몇 입력 모델에서 검증을 하지 않던 문제 수정 * refactor: 메소드 추가 분리 * refactor: 경로 추적 시 두 번의 연산을 수행하지 않도록 DTO 생성 * chore: 사용하지 않는 메소드 제거 * test: jgrapht 구현체 테스트 생성 * chore: 메소드 분리, 가독성 개선 * test: service 테스트 보완, dummy.sql 생성 * test: 최단 경로 관련 통합테스트 생성 * chore: sql 파일 위치 변경 * feat: 프로덕션 환경에서는 MySQL, 테스트 환경에서는 H2를 사용하도록 변경 * fix: 더미 데이터 사용에 따른 테스트 코드 수정 * chore: .http 파일 삭제 * feat: swagger를 통한 API 명세 자동화 * chore: 메소드명 변경 * docs: 고민사항 기재 * chore: 클래스명 변경 * refactor: `@Validated` 어노테이션을 통한 검증 * chore: 구현체가 있으므로 인터페이스에서 `@Component` 제거 * chore: 메소드 인자명 변경 * chore: 불필요한 `support` 패키지 삭제 * refactor: repository의 insert가 도메인 엔티티를 반환하도록 수정 * test: Repository 테스트 보완 의존관계가 복잡한 경우 mock 사용 * refactor: 출력 모델에서 도메인 엔티티 제거 * refactor: 중복된 의미를 가지는 예외객체 제거 * refactor: 요금 계산 로직을 enum으로 수행 * refactor: 입력 모델 규칙 위반에 대한 예외처리 생성 * chore: 불필요한 `@Valid` 제거 * docs: 리팩토링 기록 작성 및 아키텍쳐 이미지 변경 * refactor: Repository가 H2와 관련이 없기에 네이밍 수정 --- .gitignore | 4 +- README.md | 64 +++- build.gradle | 8 +- img.png | Bin 0 -> 22575 bytes .../{ => core}/domain/Distance.java | 6 +- .../subway/application/core/domain/Fare.java | 60 +++ .../application/{ => core}/domain/Line.java | 10 +- .../{ => core}/domain/LineProperty.java | 2 +- .../application/core/domain/RouteMap.java | 102 +++++ .../{ => core}/domain/Section.java | 4 +- .../{ => core}/domain/Station.java | 6 +- .../exception/CircularRouteException.java | 2 +- .../DistanceNotPositiveException.java | 10 + .../exception/ExpectedException.java | 2 +- .../FareCantCalculatedException.java | 10 + .../exception/RouteNotConnectedException.java | 2 +- .../exception/SectionConnectException.java | 2 +- .../StationAlreadyExistsException.java | 2 +- .../exception/StationConnectException.java | 2 +- .../exception/StationNotExistsException.java | 2 +- .../exception/StationTooFarException.java | 2 +- .../core/service/JourneyService.java | 61 +++ .../core/service/LinePropertyService.java | 53 +++ .../application/core/service/LineService.java | 71 ++++ .../core/service/StationService.java | 52 +++ .../service/dto/in/DeleteStationCommand.java | 24 ++ .../service/dto/in/EnrollStationCommand.java | 39 ++ .../core/service/dto/in/IdCommand.java | 17 + .../core/service/dto/in/JourneyCommand.java | 25 ++ .../dto/in/SaveLinePropertyCommand.java | 24 ++ .../service/dto/in/SaveStationCommand.java | 21 + .../dto/in/UpdateLinePropertyCommand.java | 26 ++ .../service/dto/in/UpdateStationCommand.java | 23 ++ .../core/service/dto/out/JourneyResult.java | 30 ++ .../service/dto/out/LinePropertyResult.java | 34 ++ .../core/service/dto/out/PathFindResult.java | 24 ++ .../core/service/dto/out/StationResult.java | 26 ++ .../subway/application/domain/RouteMap.java | 90 ----- .../exception/DistanceExceedException.java | 10 - .../LinePropertyRepository.java | 4 +- .../{repository => port}/LineRepository.java | 6 +- .../subway/application/port/PathFinder.java | 13 + .../StationRepository.java | 4 +- .../service/LinePropertyService.java | 52 --- .../application/service/LineService.java | 64 ---- .../application/service/StationService.java | 47 --- .../java/subway/config/SwaggerConfig.java | 50 +++ .../subway/config/WebMvcConfiguration.java | 22 ++ .../dao/LineDao.java} | 32 +- .../dao/SectionDao.java | 16 +- .../dao/StationDao.java | 24 +- .../entity/LineRow.java} | 6 +- .../entity}/SectionRow.java | 20 +- .../entity}/StationRow.java | 2 +- .../graph/JgraphtPathFinder.java | 44 +++ .../repository/LinePersistenceAdapter.java | 105 +++++ .../LinePropertyPersistenceAdapter.java | 47 +++ .../StationPersistenceAdapter.java} | 14 +- .../repository/H2LinePropertyRepository.java | 50 --- .../repository/H2LineRepository.java | 101 ----- .../{advice => }/ExceptionAdvice.java | 11 +- .../presentation/LoggerInterceptor.java | 32 ++ .../controller/JourneyController.java | 54 +++ .../controller/LineController.java | 36 +- .../controller/LinePropertyController.java | 52 ++- .../controller/StationController.java | 38 +- .../presentation/dto/JourneyRequest.java | 23 ++ .../presentation/dto/JourneyResponse.java | 28 ++ ...esponse.java => LinePropertyResponse.java} | 10 +- .../presentation/dto/StationResponse.java | 2 +- src/main/resources/application.properties | 3 - src/main/resources/application.yml | 11 + src/main/resources/docker-compose.yml | 18 + src/main/resources/dummy.sql | 37 ++ src/main/resources/logback-access.xml | 8 - src/main/resources/schema.sql | 16 +- src/test/java/subway/StationFixture.java | 2 +- .../java/subway/SubwayApplicationTests.java | 2 +- .../core}/domain/DistanceTest.java | 7 +- .../application/core/domain/FareTest.java | 65 ++++ .../core}/domain/LineTest.java | 20 +- .../core}/domain/RouteMapTest.java | 18 +- .../core/service/JourneyServiceTest.java | 44 +++ .../core/service/LinePropertyServiceTest.java | 103 +++++ .../core/service/LineServiceTest.java | 85 ++++ .../core/service/StationServiceTest.java | 105 +++++ .../graph/JgraphtPathFinderTest.java | 130 +++++++ .../LinePersistenceAdapterTest.java | 81 ++++ .../LinePropertyPersistenceAdapterTest.java | 96 +++++ .../StationPersistenceAdapterTest.java | 96 +++++ .../subway/integration/IntegrationTest.java | 3 - .../integration/JourneyIntegrationTest.java | 92 +++++ .../integration/LineIntegrationTest.java | 362 +++++++++++------- .../LinePropertyIntegrationTest.java | 190 +++++++++ .../integration/SubwayIntegrationTest.java | 268 ------------- .../presentation/LineControllerTest.java | 64 ---- .../controller/LineControllerTest.java | 113 ++++++ .../LinePropertyControllerTest.java | 30 +- .../StationControllerTest.java | 28 +- src/test/resources/application.yml | 7 + 100 files changed, 2970 insertions(+), 1095 deletions(-) create mode 100644 img.png rename src/main/java/subway/application/{ => core}/domain/Distance.java (86%) create mode 100644 src/main/java/subway/application/core/domain/Fare.java rename src/main/java/subway/application/{ => core}/domain/Line.java (92%) rename src/main/java/subway/application/{ => core}/domain/LineProperty.java (95%) create mode 100644 src/main/java/subway/application/core/domain/RouteMap.java rename src/main/java/subway/application/{ => core}/domain/Section.java (96%) rename src/main/java/subway/application/{ => core}/domain/Station.java (77%) rename src/main/java/subway/application/{ => core}/exception/CircularRouteException.java (84%) create mode 100644 src/main/java/subway/application/core/exception/DistanceNotPositiveException.java rename src/main/java/subway/application/{ => core}/exception/ExpectedException.java (76%) create mode 100644 src/main/java/subway/application/core/exception/FareCantCalculatedException.java rename src/main/java/subway/application/{ => core}/exception/RouteNotConnectedException.java (84%) rename src/main/java/subway/application/{ => core}/exception/SectionConnectException.java (85%) rename src/main/java/subway/application/{ => core}/exception/StationAlreadyExistsException.java (84%) rename src/main/java/subway/application/{ => core}/exception/StationConnectException.java (84%) rename src/main/java/subway/application/{ => core}/exception/StationNotExistsException.java (85%) rename src/main/java/subway/application/{ => core}/exception/StationTooFarException.java (86%) create mode 100644 src/main/java/subway/application/core/service/JourneyService.java create mode 100644 src/main/java/subway/application/core/service/LinePropertyService.java create mode 100644 src/main/java/subway/application/core/service/LineService.java create mode 100644 src/main/java/subway/application/core/service/StationService.java create mode 100644 src/main/java/subway/application/core/service/dto/in/DeleteStationCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/in/EnrollStationCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/in/IdCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/in/JourneyCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/in/SaveLinePropertyCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/in/SaveStationCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/in/UpdateLinePropertyCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/in/UpdateStationCommand.java create mode 100644 src/main/java/subway/application/core/service/dto/out/JourneyResult.java create mode 100644 src/main/java/subway/application/core/service/dto/out/LinePropertyResult.java create mode 100644 src/main/java/subway/application/core/service/dto/out/PathFindResult.java create mode 100644 src/main/java/subway/application/core/service/dto/out/StationResult.java delete mode 100644 src/main/java/subway/application/domain/RouteMap.java delete mode 100644 src/main/java/subway/application/exception/DistanceExceedException.java rename src/main/java/subway/application/{repository => port}/LinePropertyRepository.java (75%) rename src/main/java/subway/application/{repository => port}/LineRepository.java (55%) create mode 100644 src/main/java/subway/application/port/PathFinder.java rename src/main/java/subway/application/{repository => port}/StationRepository.java (74%) delete mode 100644 src/main/java/subway/application/service/LinePropertyService.java delete mode 100644 src/main/java/subway/application/service/LineService.java delete mode 100644 src/main/java/subway/application/service/StationService.java create mode 100644 src/main/java/subway/config/SwaggerConfig.java create mode 100644 src/main/java/subway/config/WebMvcConfiguration.java rename src/main/java/subway/{persistence/dao/LinePropertyDao.java => infrastructure/dao/LineDao.java} (58%) rename src/main/java/subway/{persistence => infrastructure}/dao/SectionDao.java (76%) rename src/main/java/subway/{persistence => infrastructure}/dao/StationDao.java (74%) rename src/main/java/subway/{persistence/row/LinePropertyRow.java => infrastructure/entity/LineRow.java} (73%) rename src/main/java/subway/{persistence/row => infrastructure/entity}/SectionRow.java (51%) rename src/main/java/subway/{persistence/row => infrastructure/entity}/StationRow.java (88%) create mode 100644 src/main/java/subway/infrastructure/graph/JgraphtPathFinder.java create mode 100644 src/main/java/subway/infrastructure/repository/LinePersistenceAdapter.java create mode 100644 src/main/java/subway/infrastructure/repository/LinePropertyPersistenceAdapter.java rename src/main/java/subway/{persistence/repository/H2StationRepository.java => infrastructure/repository/StationPersistenceAdapter.java} (73%) delete mode 100644 src/main/java/subway/persistence/repository/H2LinePropertyRepository.java delete mode 100644 src/main/java/subway/persistence/repository/H2LineRepository.java rename src/main/java/subway/presentation/{advice => }/ExceptionAdvice.java (78%) create mode 100644 src/main/java/subway/presentation/LoggerInterceptor.java create mode 100644 src/main/java/subway/presentation/controller/JourneyController.java create mode 100644 src/main/java/subway/presentation/dto/JourneyRequest.java create mode 100644 src/main/java/subway/presentation/dto/JourneyResponse.java rename src/main/java/subway/presentation/dto/{LineResponse.java => LinePropertyResponse.java} (54%) delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/docker-compose.yml create mode 100644 src/main/resources/dummy.sql delete mode 100644 src/main/resources/logback-access.xml rename src/test/java/subway/{ => application/core}/domain/DistanceTest.java (73%) create mode 100644 src/test/java/subway/application/core/domain/FareTest.java rename src/test/java/subway/{ => application/core}/domain/LineTest.java (92%) rename src/test/java/subway/{ => application/core}/domain/RouteMapTest.java (83%) create mode 100644 src/test/java/subway/application/core/service/JourneyServiceTest.java create mode 100644 src/test/java/subway/application/core/service/LinePropertyServiceTest.java create mode 100644 src/test/java/subway/application/core/service/LineServiceTest.java create mode 100644 src/test/java/subway/application/core/service/StationServiceTest.java create mode 100644 src/test/java/subway/infrastructure/graph/JgraphtPathFinderTest.java create mode 100644 src/test/java/subway/infrastructure/repository/LinePersistenceAdapterTest.java create mode 100644 src/test/java/subway/infrastructure/repository/LinePropertyPersistenceAdapterTest.java create mode 100644 src/test/java/subway/infrastructure/repository/StationPersistenceAdapterTest.java create mode 100644 src/test/java/subway/integration/JourneyIntegrationTest.java create mode 100644 src/test/java/subway/integration/LinePropertyIntegrationTest.java delete mode 100644 src/test/java/subway/integration/SubwayIntegrationTest.java delete mode 100644 src/test/java/subway/presentation/LineControllerTest.java create mode 100644 src/test/java/subway/presentation/controller/LineControllerTest.java rename src/test/java/subway/presentation/{ => controller}/LinePropertyControllerTest.java (77%) rename src/test/java/subway/presentation/{ => controller}/StationControllerTest.java (82%) create mode 100644 src/test/resources/application.yml diff --git a/.gitignore b/.gitignore index b44bc4d91..5543fcf67 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ build/ *.iws *.iml *.ipr -out/ +/out/ ### NetBeans ### /nbproject/private/ @@ -30,4 +30,4 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ \ No newline at end of file +.vscode/ diff --git a/README.md b/README.md index ffa56293d..1239a13ba 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,25 @@ - [x] 노선 목록 조회 API 구현 - [x] 각 노선에 포함된 역을 순서대로 보여주도록 응답을 개선한다. +- [x] 경로 조회 및 요금 조회 API 구현 + - [x] 출발역과 도착역 사이의 최단 거리 경로, 총 거리 정보를 응답한다. + - [x] 여러 노선의 환승을 고려한다. + - [x] 경로 조회 시 요금 정보를 포함하여 응답한다. + - [x] 기본운임(10KM 이내)은 1,250원이다. + - [x] 이용 거리 초과 시 추가운임을 부과한다. + - [x] 10KM부터 50KM 까지는 5KM마다 100원이 추가된다. + - [x] 50KM를 초과하면 8KM마다 100원이 추가된다. --- -# step 1 기록 +### Architecture +![img.png](img.png) + +--- + +
+ step 1 기록 +
+ - [x] `Line` 도메인 엔티티를 CRUD용으로도 사용하기도 하고, 비즈니스 로직을 수행할 때 사용하기도 해서 문제가 많았음. - [x] 문제점 1: CRUD 시점에는 완전한 도메인 객체가 아님. - [x] 문제점 2: 하나의 도메인 엔티티가 여러 개념을 내포하고 있음. @@ -91,4 +107,48 @@ - [x] N번 쿼리를 보내야 했던 문제를 1번으로 해결 - [x] 데이터베이스 레벨에서 조인을 하는 방법도 있을 듯 한데.. 관련 DAO나 Row를 또 한번 만들어줘야 하는 문제점 - [x] 조인할 때마다 새로운 DAO를 만들 것인가? -- + +
+
+ +
+ step 2 기록 +
+ +# step 2 기록 +- [x] `ON DELETE RESTRICT` 를 통해 외래키 제약조건 부여 + - [x] `SECTION` 의 참조 필드는 `STATION` 행이 삭제되었다고 해서 같이 삭제가 되거나 NULL 처리를 할 수 없음 + - [x] 도메인 제약조건이 깨지기 때문. + - [x] 따라서 서비스 로직에서 삭제 방어를 하거나, 외래키에 제약조건을 부여하는 방법이 있을 듯 함. + - [x] 이번에는 외래키 제약조건을 통해 무결성 보장. +- [x] 현재 `동일한 이름을 가진 STATION을 두 번 생성하려는 경우` 등은 테이블 제약조건에 의해 불가능하다. + - [x] 즉, 영속성 레벨에서 예외가 발생한다. + - [x] 이는 다르게 말하면 불필요한 데이터가 영속성 레이어까지 침투한다는 것이다. + - [x] 영속성 레이어까지 신뢰하지 못하는 데이터를 침투시킬것인가? 그렇다면 서비스에서 모든 무결성 검사를 진행해야 할까? 고민해보기. +- [x] 기본적으로 제공된 코드에 `logback-access.xml`이 존재했고, 이에 대한 의존성도 설정되어 있었음. + - [x] 찾아보니 컨테이너 레벨에서 로깅 기능을 제공해주는 듯 함. + - [x] 프로젝트 할 때 도입 고려해보면 좋을 듯. +- [x] 서비스 레이어에서 입력/출력 모델 생성 + - [x] 표현 계층과 비즈니스 계층의 격리를 하기 위함 +- [x] `RouteMap` 객체를 재사용 가능하게 변경 + - [x] `Station`을 들고 있는게 아닌, `Section`을 들고 있게 한다면 그래프 탐색에도 재사용 가능 +- [x] 경로를 구하는 로직이나, 요금을 계산하는 로직은 우리 시스템의 일부인데 도메인 엔티티 어디에서도 이런 기능은 존재하지 않는다. + - [x] 즉, `경로 및 요금 로직`을 외부 모듈로 다루니까 이런 문제가 발생함. + - [x] 하지만 시스템의 핵심 로직이라고 하더라도 무조건 다 도메인 엔티티에 정의되어야 하는 건 아니지 않을까? + - [x] 예를 들어, 인증도 핵심 로직이지만 도메인 엔티티에 인증 절차를 직접 정의하지는 않음. +- [x] service 테스트에 대한 고민 + - [x] `LineService`는 `LineProperty`, `Station` 데이터가 존재한다는 가정 하에 동작할 수 있다. + - [x] 그러면 Mocking을 할 것인가? 테스트 코드가 너무 더러워짐. + - [x] Mocking을 하지 않을 것인가? 그러면 더미 데이터를 매번 생성해줘야 함. + - [x] 아니면 Fake 객체를 만들 것인가? +- [x] swagger 사용, API 명세 자동화했음. + - [x] 정말 기본적인 기능만 사용했는데, 추후 프로젝트 시 적절하게 사용한다면 협업이 용이해질 듯 함. +- [x] `@Validated` 를 통한 검증 수행 + - [x] `@Valid` 단독적으로는 핸들러 메소드에서만 사용 가능. ArgumentResolver에 의해 사용되기 때문. + - [x] `@Validated`는 Spring 지원 기술(표준이 아님), AOP를 사용. + - [x] 사용하는 경우 클래스 레벨에 `@Validated`까지 붙여줘야 함. + - [x] 소스: https://mangkyu.tistory.com/174 +- [x] 요금 계산을 enum을 사용하도록 변경 + - [x] 가시성, 유지보수성 극대화 +
+
diff --git a/build.gradle b/build.gradle index 68d8f2558..83ac13109 100644 --- a/build.gradle +++ b/build.gradle @@ -13,15 +13,17 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' - - implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.jgrapht:jgrapht-core:1.0.1' + implementation 'org.springdoc:springdoc-openapi-ui:1.6.6' testImplementation 'io.rest-assured:rest-assured:4.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' } test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..52a81f9194ec97d0839d0ed5c38b2c311fcb9ba3 GIT binary patch literal 22575 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 literal 0 HcmV?d00001 diff --git a/src/main/java/subway/application/domain/Distance.java b/src/main/java/subway/application/core/domain/Distance.java similarity index 86% rename from src/main/java/subway/application/domain/Distance.java rename to src/main/java/subway/application/core/domain/Distance.java index 88851ca90..4428cc688 100644 --- a/src/main/java/subway/application/domain/Distance.java +++ b/src/main/java/subway/application/core/domain/Distance.java @@ -1,6 +1,6 @@ -package subway.application.domain; +package subway.application.core.domain; -import subway.application.exception.DistanceExceedException; +import subway.application.core.exception.DistanceNotPositiveException; import java.util.Objects; @@ -15,7 +15,7 @@ public Distance(int distance) { private void validate(int distance) { if (distance <= 0) { - throw new DistanceExceedException(); + throw new DistanceNotPositiveException(); } } diff --git a/src/main/java/subway/application/core/domain/Fare.java b/src/main/java/subway/application/core/domain/Fare.java new file mode 100644 index 000000000..57a6e630b --- /dev/null +++ b/src/main/java/subway/application/core/domain/Fare.java @@ -0,0 +1,60 @@ +package subway.application.core.domain; + +import subway.application.core.exception.FareCantCalculatedException; + +import java.util.Arrays; + +public enum Fare { + + UNDER_TEN_KM(0.0, 10.0) { + @Override + int calculateFare(double distance) { + return BASE_FARE; + } + }, + BETWEEN_TEN_AND_FIFTY(10.0, 50.0) { + @Override + int calculateFare(double distance) { + distance -= 10; + return BASE_FARE + calculateOverFareForEveryDistance(distance, REFERENCE_DISTANCE_UNDER_FIFTY); + } + }, + OVER_FIFTY(50.0, Integer.MAX_VALUE) { + @Override + int calculateFare(double distance) { + distance -= 50; + return BASE_FARE + calculateOverFareForEveryDistance(40, REFERENCE_DISTANCE_UNDER_FIFTY) + + calculateOverFareForEveryDistance(distance, REFERENCE_DISTANCE_OVER_FIFTY); + } + }; + + private static final int BASE_FARE = 1_250; + private static final double REFERENCE_DISTANCE_UNDER_FIFTY = 5.0; + private static final double REFERENCE_DISTANCE_OVER_FIFTY = 8.0; + + protected final double startPoint; + protected final double endPoint; + + Fare(double startPoint, double endPoint) { + this.startPoint = startPoint; + this.endPoint = endPoint; + } + + public static int of(double distance) { + return Arrays.stream(values()) + .filter(fareEnum -> fareEnum.matches(distance)) + .findAny() + .orElseThrow(FareCantCalculatedException::new) + .calculateFare(distance); + } + + abstract int calculateFare(double distance); + + protected int calculateOverFareForEveryDistance(double distance, double referenceDistance) { + return (int) ((Math.ceil(((int) distance - 1) / (int) referenceDistance) + 1) * 100); + } + + protected boolean matches(double distance) { + return Double.compare(startPoint, distance) <= 0 && Double.compare(distance, endPoint) <= 0; + } +} diff --git a/src/main/java/subway/application/domain/Line.java b/src/main/java/subway/application/core/domain/Line.java similarity index 92% rename from src/main/java/subway/application/domain/Line.java rename to src/main/java/subway/application/core/domain/Line.java index aaf0a8a1c..49087ea87 100644 --- a/src/main/java/subway/application/domain/Line.java +++ b/src/main/java/subway/application/core/domain/Line.java @@ -1,9 +1,9 @@ -package subway.application.domain; +package subway.application.core.domain; -import subway.application.exception.StationAlreadyExistsException; -import subway.application.exception.StationConnectException; -import subway.application.exception.StationNotExistsException; -import subway.application.exception.StationTooFarException; +import subway.application.core.exception.StationAlreadyExistsException; +import subway.application.core.exception.StationNotExistsException; +import subway.application.core.exception.StationTooFarException; +import subway.application.core.exception.StationConnectException; import java.util.List; import java.util.Objects; diff --git a/src/main/java/subway/application/domain/LineProperty.java b/src/main/java/subway/application/core/domain/LineProperty.java similarity index 95% rename from src/main/java/subway/application/domain/LineProperty.java rename to src/main/java/subway/application/core/domain/LineProperty.java index cce9e4420..8f9f6c8d1 100644 --- a/src/main/java/subway/application/domain/LineProperty.java +++ b/src/main/java/subway/application/core/domain/LineProperty.java @@ -1,4 +1,4 @@ -package subway.application.domain; +package subway.application.core.domain; import java.util.Objects; diff --git a/src/main/java/subway/application/core/domain/RouteMap.java b/src/main/java/subway/application/core/domain/RouteMap.java new file mode 100644 index 000000000..90c151927 --- /dev/null +++ b/src/main/java/subway/application/core/domain/RouteMap.java @@ -0,0 +1,102 @@ +package subway.application.core.domain; + +import subway.application.core.exception.CircularRouteException; +import subway.application.core.exception.RouteNotConnectedException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class RouteMap { + + private final List
routeMap; + + public RouteMap(List
sections) { + this.routeMap = alignedRouteOf(sections); + } + + private List
alignedRouteOf(List
sections) { + if (sections.isEmpty()) { + return Collections.emptyList(); + } + return alignSections(sections); + } + + private List
alignSections(List
sections) { + List
temporarySections = new ArrayList<>(); + Section indexSection = findFirstSection(sections); + while (canMove(sections, indexSection) && !isInnerCircle(sections, temporarySections)) { + temporarySections.add(indexSection); + indexSection = findNext(sections, indexSection); + } + temporarySections.add(indexSection); + validate(sections, temporarySections); + return temporarySections; + } + + private Section findFirstSection(List
sections) { + Station firstStation = getEndPoints(sections).stream() + .findAny() + .orElseThrow(CircularRouteException::new); + + return sections.stream() + .filter(section -> section.hasUpBound(firstStation)) + .findAny() + .orElseThrow(); + } + + private List getEndPoints(List
sections) { + List allUpBounds = sections.stream() + .map(Section::getUpBound) + .collect(Collectors.toList()); + + List allDownBounds = sections.stream() + .map(Section::getDownBound) + .collect(Collectors.toList()); + + allUpBounds.removeAll(allDownBounds); + return allUpBounds; + } + + private boolean canMove(List
sections, Section targetSection) { + return sections.stream() + .anyMatch(section -> section.getUpBound().equals(targetSection.getDownBound())); + } + + private boolean isInnerCircle(List
originalSections, List
temporarySections) { + return originalSections.size() < temporarySections.size(); + } + + private Section findNext(List
sections, Section targetSection) { + return sections.stream() + .filter(section -> section.getUpBound().equals(targetSection.getDownBound())) + .findAny() + .orElseThrow(); + } + + private void validate(List
originalSections, List
temporarySections) { + if (originalSections.size() < temporarySections.size()) { + throw new CircularRouteException(); + } + if (temporarySections.size() < originalSections.size()) { + throw new RouteNotConnectedException(); + } + } + + public List stations() { + Set alignedStations = new HashSet<>(); + routeMap.forEach(section -> { + alignedStations.add(section.getUpBound()); + alignedStations.add(section.getDownBound()); + }); + return alignedStations.stream() + .collect(Collectors.toUnmodifiableList()); + } + + public List
values() { + return Collections.unmodifiableList(routeMap); + } +} diff --git a/src/main/java/subway/application/domain/Section.java b/src/main/java/subway/application/core/domain/Section.java similarity index 96% rename from src/main/java/subway/application/domain/Section.java rename to src/main/java/subway/application/core/domain/Section.java index 319941ab6..c8637856c 100644 --- a/src/main/java/subway/application/domain/Section.java +++ b/src/main/java/subway/application/core/domain/Section.java @@ -1,6 +1,6 @@ -package subway.application.domain; +package subway.application.core.domain; -import subway.application.exception.SectionConnectException; +import subway.application.core.exception.SectionConnectException; import java.util.Objects; diff --git a/src/main/java/subway/application/domain/Station.java b/src/main/java/subway/application/core/domain/Station.java similarity index 77% rename from src/main/java/subway/application/domain/Station.java rename to src/main/java/subway/application/core/domain/Station.java index 1063c3993..a0790e9d8 100644 --- a/src/main/java/subway/application/domain/Station.java +++ b/src/main/java/subway/application/core/domain/Station.java @@ -1,4 +1,4 @@ -package subway.application.domain; +package subway.application.core.domain; import java.util.Objects; @@ -25,11 +25,11 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Station station = (Station) o; - return Objects.equals(name, station.name); + return Objects.equals(id, station.id) && Objects.equals(name, station.name); } @Override public int hashCode() { - return Objects.hash(name); + return Objects.hash(id, name); } } diff --git a/src/main/java/subway/application/exception/CircularRouteException.java b/src/main/java/subway/application/core/exception/CircularRouteException.java similarity index 84% rename from src/main/java/subway/application/exception/CircularRouteException.java rename to src/main/java/subway/application/core/exception/CircularRouteException.java index 7a9dffa7e..f6d6649e8 100644 --- a/src/main/java/subway/application/exception/CircularRouteException.java +++ b/src/main/java/subway/application/core/exception/CircularRouteException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class CircularRouteException extends ExpectedException { diff --git a/src/main/java/subway/application/core/exception/DistanceNotPositiveException.java b/src/main/java/subway/application/core/exception/DistanceNotPositiveException.java new file mode 100644 index 000000000..2179d1484 --- /dev/null +++ b/src/main/java/subway/application/core/exception/DistanceNotPositiveException.java @@ -0,0 +1,10 @@ +package subway.application.core.exception; + +public class DistanceNotPositiveException extends ExpectedException { + + private static final String MESSAGE = "거리는 음수일 수 없습니다."; + + public DistanceNotPositiveException() { + super(MESSAGE); + } +} diff --git a/src/main/java/subway/application/exception/ExpectedException.java b/src/main/java/subway/application/core/exception/ExpectedException.java similarity index 76% rename from src/main/java/subway/application/exception/ExpectedException.java rename to src/main/java/subway/application/core/exception/ExpectedException.java index 3b68391f3..b6953cbb7 100644 --- a/src/main/java/subway/application/exception/ExpectedException.java +++ b/src/main/java/subway/application/core/exception/ExpectedException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class ExpectedException extends RuntimeException { diff --git a/src/main/java/subway/application/core/exception/FareCantCalculatedException.java b/src/main/java/subway/application/core/exception/FareCantCalculatedException.java new file mode 100644 index 000000000..bd794dac5 --- /dev/null +++ b/src/main/java/subway/application/core/exception/FareCantCalculatedException.java @@ -0,0 +1,10 @@ +package subway.application.core.exception; + +public class FareCantCalculatedException extends ExpectedException { + + private static final String MESSAGE = "해당하는 거리에 맞는 요금을 찾을 수 없습니다."; + + public FareCantCalculatedException() { + super(MESSAGE); + } +} diff --git a/src/main/java/subway/application/exception/RouteNotConnectedException.java b/src/main/java/subway/application/core/exception/RouteNotConnectedException.java similarity index 84% rename from src/main/java/subway/application/exception/RouteNotConnectedException.java rename to src/main/java/subway/application/core/exception/RouteNotConnectedException.java index 56576ceca..1a7f76492 100644 --- a/src/main/java/subway/application/exception/RouteNotConnectedException.java +++ b/src/main/java/subway/application/core/exception/RouteNotConnectedException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class RouteNotConnectedException extends ExpectedException { diff --git a/src/main/java/subway/application/exception/SectionConnectException.java b/src/main/java/subway/application/core/exception/SectionConnectException.java similarity index 85% rename from src/main/java/subway/application/exception/SectionConnectException.java rename to src/main/java/subway/application/core/exception/SectionConnectException.java index 024e854e2..d6286d5f4 100644 --- a/src/main/java/subway/application/exception/SectionConnectException.java +++ b/src/main/java/subway/application/core/exception/SectionConnectException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class SectionConnectException extends ExpectedException { diff --git a/src/main/java/subway/application/exception/StationAlreadyExistsException.java b/src/main/java/subway/application/core/exception/StationAlreadyExistsException.java similarity index 84% rename from src/main/java/subway/application/exception/StationAlreadyExistsException.java rename to src/main/java/subway/application/core/exception/StationAlreadyExistsException.java index fcbccddd0..d9405bbbc 100644 --- a/src/main/java/subway/application/exception/StationAlreadyExistsException.java +++ b/src/main/java/subway/application/core/exception/StationAlreadyExistsException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class StationAlreadyExistsException extends ExpectedException { diff --git a/src/main/java/subway/application/exception/StationConnectException.java b/src/main/java/subway/application/core/exception/StationConnectException.java similarity index 84% rename from src/main/java/subway/application/exception/StationConnectException.java rename to src/main/java/subway/application/core/exception/StationConnectException.java index 0f94fcf71..9ccc47d00 100644 --- a/src/main/java/subway/application/exception/StationConnectException.java +++ b/src/main/java/subway/application/core/exception/StationConnectException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class StationConnectException extends ExpectedException { diff --git a/src/main/java/subway/application/exception/StationNotExistsException.java b/src/main/java/subway/application/core/exception/StationNotExistsException.java similarity index 85% rename from src/main/java/subway/application/exception/StationNotExistsException.java rename to src/main/java/subway/application/core/exception/StationNotExistsException.java index 352c156ba..6f5dadd8a 100644 --- a/src/main/java/subway/application/exception/StationNotExistsException.java +++ b/src/main/java/subway/application/core/exception/StationNotExistsException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class StationNotExistsException extends ExpectedException { diff --git a/src/main/java/subway/application/exception/StationTooFarException.java b/src/main/java/subway/application/core/exception/StationTooFarException.java similarity index 86% rename from src/main/java/subway/application/exception/StationTooFarException.java rename to src/main/java/subway/application/core/exception/StationTooFarException.java index 9baf387ea..e7d8ccf26 100644 --- a/src/main/java/subway/application/exception/StationTooFarException.java +++ b/src/main/java/subway/application/core/exception/StationTooFarException.java @@ -1,4 +1,4 @@ -package subway.application.exception; +package subway.application.core.exception; public class StationTooFarException extends ExpectedException { diff --git a/src/main/java/subway/application/core/service/JourneyService.java b/src/main/java/subway/application/core/service/JourneyService.java new file mode 100644 index 000000000..3861f4094 --- /dev/null +++ b/src/main/java/subway/application/core/service/JourneyService.java @@ -0,0 +1,61 @@ +package subway.application.core.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import subway.application.core.domain.Fare; +import subway.application.core.domain.Line; +import subway.application.core.domain.RouteMap; +import subway.application.core.domain.Station; +import subway.application.core.service.dto.in.JourneyCommand; +import subway.application.core.service.dto.out.JourneyResult; +import subway.application.core.service.dto.out.PathFindResult; +import subway.application.core.service.dto.out.StationResult; +import subway.application.port.LineRepository; +import subway.application.port.PathFinder; +import subway.application.port.StationRepository; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Validated +@Transactional(readOnly = true) +public class JourneyService { + + private final StationRepository stationRepository; + private final LineRepository lineRepository; + private final PathFinder pathFinder; + + public JourneyService(StationRepository stationRepository, LineRepository lineRepository, PathFinder pathFinder) { + this.stationRepository = stationRepository; + this.lineRepository = lineRepository; + this.pathFinder = pathFinder; + } + + public JourneyResult findShortestJourney(@Valid JourneyCommand journeyCommand) { + List routeMaps = getAllRouteMaps(); + PathFindResult result = findShortestPath(journeyCommand, routeMaps); + return new JourneyResult(mapToStationResults(result), result.getDistance(), + Fare.of(result.getDistance())); + } + + private List mapToStationResults(PathFindResult result) { + return result.getShortestPath().stream() + .map(StationResult::new) + .collect(Collectors.toList()); + } + + private PathFindResult findShortestPath(JourneyCommand journeyCommand, List routeMaps) { + Station departure = stationRepository.findById(journeyCommand.getDeparture()); + Station terminal = stationRepository.findById(journeyCommand.getTerminal()); + return pathFinder.findShortestPath(routeMaps, departure, terminal); + } + + private List getAllRouteMaps() { + return lineRepository.findAll().stream() + .map(Line::routeMap) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/subway/application/core/service/LinePropertyService.java b/src/main/java/subway/application/core/service/LinePropertyService.java new file mode 100644 index 000000000..a312bf0a1 --- /dev/null +++ b/src/main/java/subway/application/core/service/LinePropertyService.java @@ -0,0 +1,53 @@ +package subway.application.core.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import subway.application.core.domain.LineProperty; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.in.SaveLinePropertyCommand; +import subway.application.core.service.dto.in.UpdateLinePropertyCommand; +import subway.application.core.service.dto.out.LinePropertyResult; +import subway.application.port.LinePropertyRepository; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Validated +@Transactional +public class LinePropertyService { + + private final LinePropertyRepository linePropertyRepository; + + public LinePropertyService(LinePropertyRepository linePropertyRepository) { + this.linePropertyRepository = linePropertyRepository; + } + + public LinePropertyResult saveLineProperty(@Valid SaveLinePropertyCommand command) { + LineProperty lineProperty = linePropertyRepository.insert(command.toEntity()); + return new LinePropertyResult(lineProperty); + } + + public List findAllLineProperty() { + List allLineProperties = linePropertyRepository.findAll(); + return allLineProperties.stream() + .map(lineProperty -> new LinePropertyResult(lineProperty.getId(), + lineProperty.getName(), lineProperty.getColor())) + .collect(Collectors.toList()); + } + + public LinePropertyResult findLinePropertyById(@Valid IdCommand command) { + LineProperty lineProperty = linePropertyRepository.findById(command.getId()); + return new LinePropertyResult(lineProperty); + } + + public void updateLineProperty(@Valid UpdateLinePropertyCommand command) { + linePropertyRepository.update(command.toEntity()); + } + + public void deleteLinePropertyById(@Valid IdCommand command) { + linePropertyRepository.deleteById(command.getId()); + } +} diff --git a/src/main/java/subway/application/core/service/LineService.java b/src/main/java/subway/application/core/service/LineService.java new file mode 100644 index 000000000..13da6c93b --- /dev/null +++ b/src/main/java/subway/application/core/service/LineService.java @@ -0,0 +1,71 @@ +package subway.application.core.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import subway.application.core.domain.Distance; +import subway.application.core.domain.Line; +import subway.application.core.domain.Section; +import subway.application.core.service.dto.in.DeleteStationCommand; +import subway.application.core.service.dto.in.EnrollStationCommand; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.out.StationResult; +import subway.application.port.LineRepository; +import subway.application.port.StationRepository; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Validated +@Transactional +public class LineService { + + private final LineRepository lineRepository; + private final StationRepository stationRepository; + + public LineService(LineRepository lineRepository, StationRepository stationRepository) { + this.lineRepository = lineRepository; + this.stationRepository = stationRepository; + } + + public void enrollStation(@Valid EnrollStationCommand command) { + Line line = lineRepository.findById(command.getLineId()); + Section section = generateSection(command); + line.addSection(section); + lineRepository.insert(line); + } + + private Section generateSection(EnrollStationCommand command) { + return new Section( + stationRepository.findById(command.getUpBound()), + stationRepository.findById(command.getDownBound()), + new Distance(command.getDistance()) + ); + } + + public void deleteStation(@Valid DeleteStationCommand command) { + Line line = lineRepository.findById(command.getLineId()); + line.deleteStation(stationRepository.findById(command.getStationId())); + lineRepository.insert(line); + } + + public List findRouteMap(@Valid IdCommand command) { + Line line = lineRepository.findById(command.getId()); + return makeStationResultsOf(line); + } + + public Map> findAllRouteMap() { + List allLines = lineRepository.findAll(); + return allLines.stream() + .collect(Collectors.toMap(Line::getName, this::makeStationResultsOf)); + } + + private List makeStationResultsOf(Line line) { + return line.routeMap().stations().stream() + .map(station -> new StationResult(station.getId(), station.getName())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/subway/application/core/service/StationService.java b/src/main/java/subway/application/core/service/StationService.java new file mode 100644 index 000000000..f08d9b43e --- /dev/null +++ b/src/main/java/subway/application/core/service/StationService.java @@ -0,0 +1,52 @@ +package subway.application.core.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import subway.application.core.domain.Station; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.in.SaveStationCommand; +import subway.application.core.service.dto.in.UpdateStationCommand; +import subway.application.core.service.dto.out.StationResult; +import subway.application.port.StationRepository; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Validated +@Transactional +public class StationService { + + private final StationRepository stationRepository; + + public StationService(StationRepository stationRepository) { + this.stationRepository = stationRepository; + } + + public StationResult saveStation(@Valid SaveStationCommand command) { + Station station = stationRepository.insert(command.toEntity()); + return new StationResult(station); + } + + public StationResult findStationById(@Valid IdCommand command) { + return new StationResult(stationRepository.findById(command.getId())); + } + + public List findAllStations() { + List stations = stationRepository.findAll(); + + return stations.stream() + .map(station -> new StationResult(station.getId(), station.getName())) + .collect(Collectors.toList()); + } + + public void updateStation(@Valid UpdateStationCommand command) { + stationRepository.update(command.toEntity()); + } + + public void deleteStationById(@Valid IdCommand command) { + stationRepository.deleteById(command.getId()); + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/DeleteStationCommand.java b/src/main/java/subway/application/core/service/dto/in/DeleteStationCommand.java new file mode 100644 index 000000000..b33753827 --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/DeleteStationCommand.java @@ -0,0 +1,24 @@ +package subway.application.core.service.dto.in; + +import javax.validation.constraints.NotNull; + +public class DeleteStationCommand { + + @NotNull + private final Long lineId; + @NotNull + private final Long stationId; + + public DeleteStationCommand(Long lineId, Long stationId) { + this.lineId = lineId; + this.stationId = stationId; + } + + public Long getLineId() { + return lineId; + } + + public Long getStationId() { + return stationId; + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/EnrollStationCommand.java b/src/main/java/subway/application/core/service/dto/in/EnrollStationCommand.java new file mode 100644 index 000000000..cb25bd5e5 --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/EnrollStationCommand.java @@ -0,0 +1,39 @@ +package subway.application.core.service.dto.in; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; + +public class EnrollStationCommand { + + @NotNull + private final Long lineId; + @NotNull + private final Long upBound; + @NotNull + private final Long downBound; + @Positive + private final Integer distance; + + public EnrollStationCommand(Long lineId, Long upBound, Long downBound, Integer distance) { + this.lineId = lineId; + this.upBound = upBound; + this.downBound = downBound; + this.distance = distance; + } + + public Long getLineId() { + return lineId; + } + + public Long getUpBound() { + return upBound; + } + + public Long getDownBound() { + return downBound; + } + + public Integer getDistance() { + return distance; + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/IdCommand.java b/src/main/java/subway/application/core/service/dto/in/IdCommand.java new file mode 100644 index 000000000..e6477c3e5 --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/IdCommand.java @@ -0,0 +1,17 @@ +package subway.application.core.service.dto.in; + +import javax.validation.constraints.NotNull; + +public class IdCommand { + + @NotNull + private final Long id; + + public IdCommand(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/JourneyCommand.java b/src/main/java/subway/application/core/service/dto/in/JourneyCommand.java new file mode 100644 index 000000000..0c4548d1d --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/JourneyCommand.java @@ -0,0 +1,25 @@ +package subway.application.core.service.dto.in; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public class JourneyCommand { + + @NotNull + private final Long departure; + @NotNull + private final Long terminal; + + public JourneyCommand(Long departure, Long terminal) { + this.departure = departure; + this.terminal = terminal; + } + + public Long getDeparture() { + return departure; + } + + public Long getTerminal() { + return terminal; + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/SaveLinePropertyCommand.java b/src/main/java/subway/application/core/service/dto/in/SaveLinePropertyCommand.java new file mode 100644 index 000000000..522af3fba --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/SaveLinePropertyCommand.java @@ -0,0 +1,24 @@ +package subway.application.core.service.dto.in; + +import subway.application.core.domain.LineProperty; + +import javax.validation.constraints.NotEmpty; + +public class SaveLinePropertyCommand { + + private final Long id; + @NotEmpty + private final String name; + @NotEmpty + private final String color; + + public SaveLinePropertyCommand(String name, String color) { + this.id = null; + this.name = name; + this.color = color; + } + + public LineProperty toEntity() { + return new LineProperty(id, name, color); + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/SaveStationCommand.java b/src/main/java/subway/application/core/service/dto/in/SaveStationCommand.java new file mode 100644 index 000000000..7424be30f --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/SaveStationCommand.java @@ -0,0 +1,21 @@ +package subway.application.core.service.dto.in; + +import subway.application.core.domain.Station; + +import javax.validation.constraints.NotEmpty; + +public class SaveStationCommand { + + private final Long id; + @NotEmpty + private final String name; + + public SaveStationCommand(String name) { + this.id = null; + this.name = name; + } + + public Station toEntity() { + return new Station(id, name); + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/UpdateLinePropertyCommand.java b/src/main/java/subway/application/core/service/dto/in/UpdateLinePropertyCommand.java new file mode 100644 index 000000000..fad2abb50 --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/UpdateLinePropertyCommand.java @@ -0,0 +1,26 @@ +package subway.application.core.service.dto.in; + +import subway.application.core.domain.LineProperty; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public class UpdateLinePropertyCommand { + + @NotNull + private final Long id; + @NotEmpty + private final String name; + @NotEmpty + private final String color; + + public UpdateLinePropertyCommand(Long id, String name, String color) { + this.id = id; + this.name = name; + this.color = color; + } + + public LineProperty toEntity() { + return new LineProperty(id, name, color); + } +} diff --git a/src/main/java/subway/application/core/service/dto/in/UpdateStationCommand.java b/src/main/java/subway/application/core/service/dto/in/UpdateStationCommand.java new file mode 100644 index 000000000..9c615206e --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/in/UpdateStationCommand.java @@ -0,0 +1,23 @@ +package subway.application.core.service.dto.in; + +import subway.application.core.domain.Station; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +public class UpdateStationCommand { + + @NotNull + private final Long id; + @NotEmpty + private final String name; + + public UpdateStationCommand(Long id, String name) { + this.id = id; + this.name = name; + } + + public Station toEntity() { + return new Station(id, name); + } +} diff --git a/src/main/java/subway/application/core/service/dto/out/JourneyResult.java b/src/main/java/subway/application/core/service/dto/out/JourneyResult.java new file mode 100644 index 000000000..196ba4f87 --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/out/JourneyResult.java @@ -0,0 +1,30 @@ +package subway.application.core.service.dto.out; + +import subway.application.core.domain.Station; + +import java.util.List; + +public class JourneyResult { + + private final List path; + private final Double distance; + private final Integer fare; + + public JourneyResult(List path, Double distance, Integer fare) { + this.path = path; + this.distance = distance; + this.fare = fare; + } + + public List getPath() { + return path; + } + + public Double getDistance() { + return distance; + } + + public Integer getFare() { + return fare; + } +} diff --git a/src/main/java/subway/application/core/service/dto/out/LinePropertyResult.java b/src/main/java/subway/application/core/service/dto/out/LinePropertyResult.java new file mode 100644 index 000000000..b769c662d --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/out/LinePropertyResult.java @@ -0,0 +1,34 @@ +package subway.application.core.service.dto.out; + +import subway.application.core.domain.LineProperty; + +public class LinePropertyResult { + + private final Long id; + private final String name; + private final String color; + + public LinePropertyResult(LineProperty lineProperty) { + this.id = lineProperty.getId(); + this.name = lineProperty.getName(); + this.color = lineProperty.getColor(); + } + + public LinePropertyResult(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; + } +} diff --git a/src/main/java/subway/application/core/service/dto/out/PathFindResult.java b/src/main/java/subway/application/core/service/dto/out/PathFindResult.java new file mode 100644 index 000000000..952aa4ef8 --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/out/PathFindResult.java @@ -0,0 +1,24 @@ +package subway.application.core.service.dto.out; + +import subway.application.core.domain.Station; + +import java.util.List; + +public class PathFindResult { + + private final List shortestPath; + private final Double distance; + + public PathFindResult(List shortestPath, Double distance) { + this.shortestPath = shortestPath; + this.distance = distance; + } + + public List getShortestPath() { + return shortestPath; + } + + public Double getDistance() { + return distance; + } +} diff --git a/src/main/java/subway/application/core/service/dto/out/StationResult.java b/src/main/java/subway/application/core/service/dto/out/StationResult.java new file mode 100644 index 000000000..3273b94b0 --- /dev/null +++ b/src/main/java/subway/application/core/service/dto/out/StationResult.java @@ -0,0 +1,26 @@ +package subway.application.core.service.dto.out; + +import subway.application.core.domain.Station; + +public class StationResult { + + private final Long id; + private final String name; + + public StationResult(Station station) { + this(station.getId(), station.getName()); + } + + public StationResult(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/subway/application/domain/RouteMap.java b/src/main/java/subway/application/domain/RouteMap.java deleted file mode 100644 index ab2ae62de..000000000 --- a/src/main/java/subway/application/domain/RouteMap.java +++ /dev/null @@ -1,90 +0,0 @@ -package subway.application.domain; - -import subway.application.exception.CircularRouteException; -import subway.application.exception.RouteNotConnectedException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -public class RouteMap { - - private final List routeMap; - - public RouteMap(List
sections) { - this.routeMap = routeOf(sections); - } - - private List routeOf(List
sections) { - if (sections.isEmpty()) { - return Collections.emptyList(); - } - return linkStations(sections); - } - - private List linkStations(List
sections) { - List stations = new ArrayList<>(); - Station indexStation = findFirstStation(sections); - while (hasNextStation(sections, indexStation) && !isInnerCircle(sections, stations)) { - stations.add(indexStation); - indexStation = findNext(sections, indexStation); - } - stations.add(indexStation); - validate(sections, stations); - return stations; - } - - private Station findFirstStation(List
sections) { - List endPoints = getEndPoints(sections); - - return endPoints.stream() - .findAny() - .orElseThrow(CircularRouteException::new); - } - - private List getEndPoints(List
sections) { - List allUpBounds = sections.stream() - .map(Section::getUpBound) - .collect(Collectors.toList()); - - List allDownBounds = sections.stream() - .map(Section::getDownBound) - .collect(Collectors.toList()); - - allUpBounds.removeAll(allDownBounds); - return allUpBounds; - } - - private boolean hasNextStation(List
sections, Station targetStation) { - return sections.stream() - .anyMatch(section -> section.getUpBound().equals(targetStation)); - } - - private boolean isInnerCircle(List
sections, List stations) { - int maxStationSize = sections.size() + 1; - return maxStationSize < stations.size(); - } - - private Station findNext(List
sections, Station targetStation) { - return sections.stream() - .filter(section -> section.getUpBound().equals(targetStation)) - .map(Section::getDownBound) - .findAny() - .orElseThrow(); - } - - private void validate(List
sections, List stations) { - int maxStationSize = sections.size() + 1; - if (maxStationSize < stations.size()) { - throw new CircularRouteException(); - } - if (maxStationSize > stations.size()) { - throw new RouteNotConnectedException(); - } - } - - public List value() { - return Collections.unmodifiableList(routeMap); - } -} diff --git a/src/main/java/subway/application/exception/DistanceExceedException.java b/src/main/java/subway/application/exception/DistanceExceedException.java deleted file mode 100644 index 9a78e32e1..000000000 --- a/src/main/java/subway/application/exception/DistanceExceedException.java +++ /dev/null @@ -1,10 +0,0 @@ -package subway.application.exception; - -public class DistanceExceedException extends ExpectedException { - - private static final String MESSAGE = "역 간의 거리는 양수여야 합니다."; - - public DistanceExceedException() { - super(MESSAGE); - } -} diff --git a/src/main/java/subway/application/repository/LinePropertyRepository.java b/src/main/java/subway/application/port/LinePropertyRepository.java similarity index 75% rename from src/main/java/subway/application/repository/LinePropertyRepository.java rename to src/main/java/subway/application/port/LinePropertyRepository.java index eb19d334b..3666bd94e 100644 --- a/src/main/java/subway/application/repository/LinePropertyRepository.java +++ b/src/main/java/subway/application/port/LinePropertyRepository.java @@ -1,6 +1,6 @@ -package subway.application.repository; +package subway.application.port; -import subway.application.domain.LineProperty; +import subway.application.core.domain.LineProperty; import java.util.List; diff --git a/src/main/java/subway/application/repository/LineRepository.java b/src/main/java/subway/application/port/LineRepository.java similarity index 55% rename from src/main/java/subway/application/repository/LineRepository.java rename to src/main/java/subway/application/port/LineRepository.java index f02882307..08ceddb25 100644 --- a/src/main/java/subway/application/repository/LineRepository.java +++ b/src/main/java/subway/application/port/LineRepository.java @@ -1,12 +1,12 @@ -package subway.application.repository; +package subway.application.port; -import subway.application.domain.Line; +import subway.application.core.domain.Line; import java.util.List; public interface LineRepository { - void insert(Line line); + Line insert(Line line); Line findById(Long linePropertyId); diff --git a/src/main/java/subway/application/port/PathFinder.java b/src/main/java/subway/application/port/PathFinder.java new file mode 100644 index 000000000..96d31bff9 --- /dev/null +++ b/src/main/java/subway/application/port/PathFinder.java @@ -0,0 +1,13 @@ +package subway.application.port; + +import org.springframework.stereotype.Component; +import subway.application.core.domain.RouteMap; +import subway.application.core.domain.Station; +import subway.application.core.service.dto.out.PathFindResult; + +import java.util.List; + +public interface PathFinder { + + PathFindResult findShortestPath(List routeMap, Station departure, Station terminal); +} diff --git a/src/main/java/subway/application/repository/StationRepository.java b/src/main/java/subway/application/port/StationRepository.java similarity index 74% rename from src/main/java/subway/application/repository/StationRepository.java rename to src/main/java/subway/application/port/StationRepository.java index fbc0f9552..da2b8ec47 100644 --- a/src/main/java/subway/application/repository/StationRepository.java +++ b/src/main/java/subway/application/port/StationRepository.java @@ -1,6 +1,6 @@ -package subway.application.repository; +package subway.application.port; -import subway.application.domain.Station; +import subway.application.core.domain.Station; import java.util.List; diff --git a/src/main/java/subway/application/service/LinePropertyService.java b/src/main/java/subway/application/service/LinePropertyService.java deleted file mode 100644 index 668790c64..000000000 --- a/src/main/java/subway/application/service/LinePropertyService.java +++ /dev/null @@ -1,52 +0,0 @@ -package subway.application.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import subway.application.domain.LineProperty; -import subway.application.repository.LinePropertyRepository; -import subway.presentation.dto.LineRequest; -import subway.presentation.dto.LineResponse; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -@Transactional -public class LinePropertyService { - - private final LinePropertyRepository linePropertyRepository; - - public LinePropertyService(LinePropertyRepository linePropertyRepository) { - this.linePropertyRepository = linePropertyRepository; - } - - public LineResponse saveLine(LineRequest request) { - LineProperty lineProperty = linePropertyRepository.insert( - new LineProperty(null, request.getName(), request.getColor())); - return LineResponse.of(lineProperty); - } - - public List findLineResponses() { - List allLineProperties = findLines(); - return allLineProperties.stream() - .map(LineResponse::of) - .collect(Collectors.toList()); - } - - public List findLines() { - return linePropertyRepository.findAll(); - } - - public LineResponse findLineResponseById(Long id) { - LineProperty lineProperty = linePropertyRepository.findById(id); - return LineResponse.of(lineProperty); - } - - public void updateLine(Long id, LineRequest lineUpdateRequest) { - linePropertyRepository.update(new LineProperty(id, lineUpdateRequest.getName(), lineUpdateRequest.getColor())); - } - - public void deleteLineById(Long id) { - linePropertyRepository.deleteById(id); - } -} diff --git a/src/main/java/subway/application/service/LineService.java b/src/main/java/subway/application/service/LineService.java deleted file mode 100644 index 19c7b3554..000000000 --- a/src/main/java/subway/application/service/LineService.java +++ /dev/null @@ -1,64 +0,0 @@ -package subway.application.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import subway.application.domain.Distance; -import subway.application.domain.Line; -import subway.application.domain.Section; -import subway.application.repository.LineRepository; -import subway.application.repository.StationRepository; -import subway.presentation.dto.StationEnrollRequest; -import subway.presentation.dto.StationResponse; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Service -@Transactional -public class LineService { - - private final LineRepository lineRepository; - private final StationRepository stationRepository; - - public LineService(LineRepository lineRepository, StationRepository stationRepository) { - this.lineRepository = lineRepository; - this.stationRepository = stationRepository; - } - - public void enrollStation(Long lineId, StationEnrollRequest request) { - Line line = lineRepository.findById(lineId); - Section section = new Section( - stationRepository.findById(request.getUpBound()), - stationRepository.findById(request.getDownBound()), - new Distance(request.getDistance()) - ); - line.addSection(section); - lineRepository.insert(line); - } - - public void deleteStation(Long lineId, Long stationId) { - Line line = lineRepository.findById(lineId); - - line.deleteStation(stationRepository.findById(stationId)); - lineRepository.insert(line); - } - - public List findRouteMap(Long lineId) { - Line line = lineRepository.findById(lineId); - return makeRouteMapResponseOf(line); - } - - public Map> findAllRouteMap() { - List allLines = lineRepository.findAll(); - - return allLines.stream() - .collect(Collectors.toMap(Line::getName, this::makeRouteMapResponseOf)); - } - - private List makeRouteMapResponseOf(Line line) { - return line.routeMap().value().stream() - .map(station -> new StationResponse(station.getId(), station.getName())) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/subway/application/service/StationService.java b/src/main/java/subway/application/service/StationService.java deleted file mode 100644 index 8c167689b..000000000 --- a/src/main/java/subway/application/service/StationService.java +++ /dev/null @@ -1,47 +0,0 @@ -package subway.application.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import subway.application.domain.Station; -import subway.application.repository.StationRepository; -import subway.presentation.dto.StationRequest; -import subway.presentation.dto.StationResponse; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -@Transactional -public class StationService { - - private final StationRepository stationRepository; - - public StationService(StationRepository stationRepository) { - this.stationRepository = stationRepository; - } - - public StationResponse saveStation(StationRequest stationRequest) { - Station station = stationRepository.insert(new Station(null, stationRequest.getName())); - return StationResponse.of(station); - } - - public StationResponse findStationResponseById(Long id) { - return StationResponse.of(stationRepository.findById(id)); - } - - public List findAllStationResponses() { - List stations = stationRepository.findAll(); - - return stations.stream() - .map(StationResponse::of) - .collect(Collectors.toList()); - } - - public void updateStation(Long id, StationRequest stationRequest) { - stationRepository.update(new Station(id, stationRequest.getName())); - } - - public void deleteStationById(Long id) { - stationRepository.deleteById(id); - } -} diff --git a/src/main/java/subway/config/SwaggerConfig.java b/src/main/java/subway/config/SwaggerConfig.java new file mode 100644 index 000000000..42a37173b --- /dev/null +++ b/src/main/java/subway/config/SwaggerConfig.java @@ -0,0 +1,50 @@ +package subway.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public GroupedOpenApi stationApi() { + return GroupedOpenApi.builder() + .group("STATION") + .pathsToMatch("/stations/**") + .build(); + } + + @Bean + public GroupedOpenApi linePropertyApi() { + return GroupedOpenApi.builder() + .group("LINE PROPERTY") + .pathsToMatch("/lines/**") + .build(); + } + + @Bean + public GroupedOpenApi lineApi() { + return GroupedOpenApi.builder() + .group("LINE") + .pathsToMatch("/subway/**") + .build(); + } + + @Bean + public GroupedOpenApi journeyApi() { + return GroupedOpenApi.builder() + .group("JOURNEY") + .pathsToMatch("/journey/**") + .build(); + } + + @Bean + public OpenAPI springShopOpenAPI() { + return new OpenAPI() + .info(new Info().title("subway-path API") + .description("지하철 노선도 미션 API입니다.")); + } +} diff --git a/src/main/java/subway/config/WebMvcConfiguration.java b/src/main/java/subway/config/WebMvcConfiguration.java new file mode 100644 index 000000000..0a76271e0 --- /dev/null +++ b/src/main/java/subway/config/WebMvcConfiguration.java @@ -0,0 +1,22 @@ +package subway.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import subway.presentation.LoggerInterceptor; + +@Configuration +public class WebMvcConfiguration implements WebMvcConfigurer { + + private final LoggerInterceptor loggerInterceptor; + + public WebMvcConfiguration(LoggerInterceptor loggerInterceptor) { + this.loggerInterceptor = loggerInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(loggerInterceptor) + .addPathPatterns("/**"); + } +} diff --git a/src/main/java/subway/persistence/dao/LinePropertyDao.java b/src/main/java/subway/infrastructure/dao/LineDao.java similarity index 58% rename from src/main/java/subway/persistence/dao/LinePropertyDao.java rename to src/main/java/subway/infrastructure/dao/LineDao.java index 6c3de0655..3c736edcf 100644 --- a/src/main/java/subway/persistence/dao/LinePropertyDao.java +++ b/src/main/java/subway/infrastructure/dao/LineDao.java @@ -1,10 +1,10 @@ -package subway.persistence.dao; +package subway.infrastructure.dao; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; -import subway.persistence.row.LinePropertyRow; +import subway.infrastructure.entity.LineRow; import javax.sql.DataSource; import java.util.HashMap; @@ -12,51 +12,51 @@ import java.util.Map; @Repository -public class LinePropertyDao { +public class LineDao { private final JdbcTemplate jdbcTemplate; private final SimpleJdbcInsert insertAction; - private final RowMapper rowMapper = (rs, rowNum) -> - new LinePropertyRow( + private final RowMapper rowMapper = (rs, rowNum) -> + new LineRow( rs.getLong("id"), rs.getString("name"), rs.getString("color") ); - public LinePropertyDao(JdbcTemplate jdbcTemplate, DataSource dataSource) { + public LineDao(JdbcTemplate jdbcTemplate, DataSource dataSource) { this.jdbcTemplate = jdbcTemplate; this.insertAction = new SimpleJdbcInsert(dataSource) - .withTableName("line_property") + .withTableName("line") .usingGeneratedKeyColumns("id"); } - public LinePropertyRow insert(LinePropertyRow row) { + public LineRow insert(LineRow row) { Map params = new HashMap<>(); params.put("id", row.getId()); params.put("name", row.getName()); params.put("color", row.getColor()); Long lineId = insertAction.executeAndReturnKey(params).longValue(); - return new LinePropertyRow(lineId, row.getName(), row.getColor()); + return new LineRow(lineId, row.getName(), row.getColor()); } - public List selectAll() { - String sql = "select id, name, color from line_property"; + public List selectAll() { + String sql = "select id, name, color from line"; return jdbcTemplate.query(sql, rowMapper); } - public LinePropertyRow findById(Long id) { - String sql = "select id, name, color from line_property WHERE id = ?"; + public LineRow findById(Long id) { + String sql = "select id, name, color from line WHERE id = ?"; return jdbcTemplate.queryForObject(sql, rowMapper, id); } - public void update(LinePropertyRow row) { - String sql = "update line_property set name = ?, color = ? where id = ?"; + public void update(LineRow row) { + String sql = "update line set name = ?, color = ? where id = ?"; jdbcTemplate.update(sql, new Object[]{row.getName(), row.getColor(), row.getId()}); } public void deleteById(Long id) { - String sql = "delete from line_property where id = ?"; + String sql = "delete from line where id = ?"; jdbcTemplate.update(sql, id); } } diff --git a/src/main/java/subway/persistence/dao/SectionDao.java b/src/main/java/subway/infrastructure/dao/SectionDao.java similarity index 76% rename from src/main/java/subway/persistence/dao/SectionDao.java rename to src/main/java/subway/infrastructure/dao/SectionDao.java index fc509e963..1565fa61f 100644 --- a/src/main/java/subway/persistence/dao/SectionDao.java +++ b/src/main/java/subway/infrastructure/dao/SectionDao.java @@ -1,9 +1,9 @@ -package subway.persistence.dao; +package subway.infrastructure.dao; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; -import subway.persistence.row.SectionRow; +import subway.infrastructure.entity.SectionRow; import java.util.List; @@ -17,8 +17,8 @@ public class SectionDao { private final RowMapper rowMapper = (rs, cn) -> new SectionRow( rs.getLong("id"), rs.getLong("line_id"), - rs.getString("up_bound"), - rs.getString("down_bound"), + rs.getLong("up_bound"), + rs.getLong("down_bound"), rs.getInt("distance") ); @@ -26,9 +26,9 @@ public SectionDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - public List selectAllOfLinePropertyId(Long linePropertyId) { + public List selectAllOfLineId(Long lineId) { String sectionSql = "select id, line_id, up_bound, down_bound, distance from section where line_id = ?"; - return jdbcTemplate.query(sectionSql, rowMapper, linePropertyId); + return jdbcTemplate.query(sectionSql, rowMapper, lineId); } public void insertAll(List rows) { @@ -37,8 +37,8 @@ public void insertAll(List rows) { jdbcTemplate.batchUpdate(sql, rows, BATCH_SIZE, (ps, entity) -> { ps.setLong(1, entity.getLineId()); - ps.setString(2, entity.getUpBound()); - ps.setString(3, entity.getDownBound()); + ps.setLong(2, entity.getUpBound()); + ps.setLong(3, entity.getDownBound()); ps.setInt(4, entity.getDistance()); }); } diff --git a/src/main/java/subway/persistence/dao/StationDao.java b/src/main/java/subway/infrastructure/dao/StationDao.java similarity index 74% rename from src/main/java/subway/persistence/dao/StationDao.java rename to src/main/java/subway/infrastructure/dao/StationDao.java index 60a527a6c..625207bf1 100644 --- a/src/main/java/subway/persistence/dao/StationDao.java +++ b/src/main/java/subway/infrastructure/dao/StationDao.java @@ -1,4 +1,4 @@ -package subway.persistence.dao; +package subway.infrastructure.dao; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -6,7 +6,7 @@ import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; -import subway.persistence.row.StationRow; +import subway.infrastructure.entity.StationRow; import javax.sql.DataSource; import java.util.Collections; @@ -41,34 +41,34 @@ public StationRow insert(StationRow row) { } public List selectAll() { - String sql = "select * from STATION"; + String sql = "select * from station"; return jdbcTemplate.query(sql, rowMapper); } public StationRow selectById(Long id) { - String sql = "select * from STATION where id = ?"; + String sql = "select * from station where id = ?"; return jdbcTemplate.queryForObject(sql, rowMapper, id); } - public Map selectKeyValueSetWhereNameIn(List names) { - String inSql = String.join(",", Collections.nCopies(names.size(), "?")); - String sql = String.format("select id, name from station where name in (%s)", inSql); + public Map selectKeyValueSetWhereIdIn(List ids) { + String inSql = String.join(",", Collections.nCopies(ids.size(), "?")); + String sql = String.format("select id, name from station where id in (%s)", inSql); - List> nameIdKeyValue = jdbcTemplate.query(sql, - (rs, cn) -> Map.entry(rs.getString("name"), rs.getLong("id")), - names.toArray()); + List> nameIdKeyValue = jdbcTemplate.query(sql, + (rs, cn) -> Map.entry(rs.getLong("id"), rs.getString("name")), + ids.toArray()); return nameIdKeyValue.stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } public void update(StationRow row) { - String sql = "update STATION set name = ? where id = ?"; + String sql = "update station set name = ? where id = ?"; jdbcTemplate.update(sql, new Object[]{row.getName(), row.getId()}); } public void deleteById(Long id) { - String sql = "delete from STATION where id = ?"; + String sql = "delete from station where id = ?"; jdbcTemplate.update(sql, id); } } diff --git a/src/main/java/subway/persistence/row/LinePropertyRow.java b/src/main/java/subway/infrastructure/entity/LineRow.java similarity index 73% rename from src/main/java/subway/persistence/row/LinePropertyRow.java rename to src/main/java/subway/infrastructure/entity/LineRow.java index 85dcac4c5..2e67529d2 100644 --- a/src/main/java/subway/persistence/row/LinePropertyRow.java +++ b/src/main/java/subway/infrastructure/entity/LineRow.java @@ -1,12 +1,12 @@ -package subway.persistence.row; +package subway.infrastructure.entity; -public class LinePropertyRow { +public class LineRow { private final Long id; private final String name; private final String color; - public LinePropertyRow(Long id, String name, String color) { + public LineRow(Long id, String name, String color) { this.id = id; this.name = name; this.color = color; diff --git a/src/main/java/subway/persistence/row/SectionRow.java b/src/main/java/subway/infrastructure/entity/SectionRow.java similarity index 51% rename from src/main/java/subway/persistence/row/SectionRow.java rename to src/main/java/subway/infrastructure/entity/SectionRow.java index 6faaa406e..dd5e4bad0 100644 --- a/src/main/java/subway/persistence/row/SectionRow.java +++ b/src/main/java/subway/infrastructure/entity/SectionRow.java @@ -1,18 +1,18 @@ -package subway.persistence.row; +package subway.infrastructure.entity; public class SectionRow { private final Long id; private final Long lineId; - private final String left; - private final String right; + private final Long upBound; + private final Long downBound; private final Integer distance; - public SectionRow(Long id, Long lineId, String left, String right, Integer distance) { + public SectionRow(Long id, Long lineId, Long upBound, Long downBound, Integer distance) { this.id = id; this.lineId = lineId; - this.left = left; - this.right = right; + this.upBound = upBound; + this.downBound = downBound; this.distance = distance; } @@ -24,12 +24,12 @@ public Long getLineId() { return lineId; } - public String getUpBound() { - return left; + public Long getUpBound() { + return upBound; } - public String getDownBound() { - return right; + public Long getDownBound() { + return downBound; } public Integer getDistance() { diff --git a/src/main/java/subway/persistence/row/StationRow.java b/src/main/java/subway/infrastructure/entity/StationRow.java similarity index 88% rename from src/main/java/subway/persistence/row/StationRow.java rename to src/main/java/subway/infrastructure/entity/StationRow.java index d4fe77371..81625b15e 100644 --- a/src/main/java/subway/persistence/row/StationRow.java +++ b/src/main/java/subway/infrastructure/entity/StationRow.java @@ -1,4 +1,4 @@ -package subway.persistence.row; +package subway.infrastructure.entity; public class StationRow { diff --git a/src/main/java/subway/infrastructure/graph/JgraphtPathFinder.java b/src/main/java/subway/infrastructure/graph/JgraphtPathFinder.java new file mode 100644 index 000000000..1aa9d58a3 --- /dev/null +++ b/src/main/java/subway/infrastructure/graph/JgraphtPathFinder.java @@ -0,0 +1,44 @@ +package subway.infrastructure.graph; + +import org.jgrapht.GraphPath; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; +import org.springframework.stereotype.Component; +import subway.application.core.domain.RouteMap; +import subway.application.core.domain.Section; +import subway.application.core.domain.Station; +import subway.application.core.service.dto.out.PathFindResult; +import subway.application.port.PathFinder; + +import java.util.List; + +@Component +public class JgraphtPathFinder implements PathFinder { + + @Override + public PathFindResult findShortestPath(List routeMap, Station departure, Station terminal) { + WeightedMultigraph graph = setupGraph(routeMap); + DijkstraShortestPath shortestPath = new DijkstraShortestPath<>(graph); + GraphPath path = shortestPath.getPath(departure, terminal); + return new PathFindResult(path.getVertexList(), path.getWeight()); + } + + private WeightedMultigraph setupGraph(List routeMap) { + WeightedMultigraph graph = new WeightedMultigraph<>(DefaultWeightedEdge.class); + routeMap.stream() + .map(RouteMap::values) + .forEach(stations -> applyRouteToGraph(graph, stations)); + return graph; + } + + private void applyRouteToGraph(WeightedMultigraph graph, List
sections) { + sections.forEach(section -> { + Station upBound = section.getUpBound(); + Station downBound = section.getDownBound(); + graph.addVertex(upBound); + graph.addVertex(downBound); + graph.setEdgeWeight(graph.addEdge(upBound, downBound), section.getDistance().value()); + }); + } +} diff --git a/src/main/java/subway/infrastructure/repository/LinePersistenceAdapter.java b/src/main/java/subway/infrastructure/repository/LinePersistenceAdapter.java new file mode 100644 index 000000000..ce28ef831 --- /dev/null +++ b/src/main/java/subway/infrastructure/repository/LinePersistenceAdapter.java @@ -0,0 +1,105 @@ +package subway.infrastructure.repository; + +import org.springframework.stereotype.Repository; +import subway.application.port.LineRepository; +import subway.application.core.domain.Distance; +import subway.application.core.domain.Line; +import subway.application.core.domain.LineProperty; +import subway.application.core.domain.Section; +import subway.application.core.domain.Station; +import subway.infrastructure.dao.LineDao; +import subway.infrastructure.dao.SectionDao; +import subway.infrastructure.dao.StationDao; +import subway.infrastructure.entity.LineRow; +import subway.infrastructure.entity.SectionRow; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Repository +public class LinePersistenceAdapter implements LineRepository { + + private final SectionDao sectionDao; + private final StationDao stationDao; + private final LineDao lineDao; + + public LinePersistenceAdapter(SectionDao sectionDao, LineDao lineDao, StationDao stationDao) { + this.sectionDao = sectionDao; + this.stationDao = stationDao; + this.lineDao = lineDao; + } + + public Line findById(Long lineId) { + List sectionEntities = sectionDao.selectAllOfLineId(lineId); + LineRow lineRow = lineDao.findById(lineId); + List
sections = generateSectionsFrom(sectionEntities); + + return new Line(new LineProperty(lineRow.getId(), lineRow.getName(), + lineRow.getColor()), sections); + } + + private List
generateSectionsFrom(List sectionEntities) { + Map stationIdNameSet = getStationIdNameSet(sectionEntities); + return sectionEntities.stream() + .map(sectionRow -> new Section( + new Station(sectionRow.getUpBound(), stationIdNameSet.get(sectionRow.getUpBound())), + new Station(sectionRow.getDownBound(), stationIdNameSet.get(sectionRow.getDownBound())), + new Distance(sectionRow.getDistance()))) + .collect(Collectors.toList()); + } + + private Map getStationIdNameSet(List sectionRows) { + if (sectionRows.isEmpty()) { + return Collections.emptyMap(); + } + return stationDao.selectKeyValueSetWhereIdIn(getStationIds(sectionRows)); + } + + private List getStationIds(List sectionEntities) { + Set stationNames = new HashSet<>(); + for (SectionRow sectionRow : sectionEntities) { + stationNames.add(sectionRow.getUpBound()); + stationNames.add(sectionRow.getDownBound()); + } + return new ArrayList<>(stationNames); + } + + public List findAll() { + List lineRows = lineDao.selectAll(); + List sectionRows = sectionDao.selectAll(); + Map stationIds = getStationIdNameSet(sectionRows); + + return lineRows.stream() + .map(lineRow -> new Line( + new LineProperty(lineRow.getId(), lineRow.getName(), lineRow.getColor()), + findSectionsWithLineId(sectionRows, stationIds, lineRow))) + .collect(Collectors.toList()); + } + + private List
findSectionsWithLineId(List sectionRows, + Map stationIds, LineRow lineRow) { + return sectionRows.stream() + .filter(sectionRow -> lineRow.getId().equals(sectionRow.getLineId())) + .map(sectionRow -> new Section( + new Station(sectionRow.getUpBound(), stationIds.get(sectionRow.getUpBound())), + new Station(sectionRow.getDownBound(), stationIds.get(sectionRow.getDownBound())), + new Distance(sectionRow.getDistance()))) + .collect(Collectors.toList()); + } + + public Line insert(Line line) { + List sectionRows = line.getSections().stream() + .map(section -> new SectionRow(null, line.getId(), section.getUpBound().getId(), + section.getDownBound().getId(), section.getDistance().value())) + .collect(Collectors.toList()); + + sectionDao.removeSections(line.getId()); + sectionDao.insertAll(sectionRows); + return findById(line.getId()); + } +} diff --git a/src/main/java/subway/infrastructure/repository/LinePropertyPersistenceAdapter.java b/src/main/java/subway/infrastructure/repository/LinePropertyPersistenceAdapter.java new file mode 100644 index 000000000..a3d93dee1 --- /dev/null +++ b/src/main/java/subway/infrastructure/repository/LinePropertyPersistenceAdapter.java @@ -0,0 +1,47 @@ +package subway.infrastructure.repository; + +import org.springframework.stereotype.Repository; +import subway.application.core.domain.LineProperty; +import subway.application.port.LinePropertyRepository; +import subway.infrastructure.dao.LineDao; +import subway.infrastructure.entity.LineRow; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +public class LinePropertyPersistenceAdapter implements LinePropertyRepository { + + private final LineDao lineDao; + + public LinePropertyPersistenceAdapter(LineDao lineDao) { + this.lineDao = lineDao; + } + + public LineProperty insert(LineProperty lineProperty) { + LineRow row = lineDao.insert( + new LineRow(lineProperty.getId(), lineProperty.getName(), lineProperty.getColor())); + return new LineProperty(row.getId(), row.getName(), row.getColor()); + } + + public List findAll() { + List rows = lineDao.selectAll(); + + return rows.stream() + .map(row -> new LineProperty(row.getId(), row.getName(), row.getColor())) + .collect(Collectors.toList()); + } + + public LineProperty findById(Long id) { + LineRow row = lineDao.findById(id); + return new LineProperty(row.getId(), row.getName(), row.getColor()); + } + + public void update(LineProperty lineProperty) { + lineDao.update(new LineRow(lineProperty.getId(), lineProperty.getName(), lineProperty.getColor())); + } + + public void deleteById(Long id) { + lineDao.deleteById(id); + } +} diff --git a/src/main/java/subway/persistence/repository/H2StationRepository.java b/src/main/java/subway/infrastructure/repository/StationPersistenceAdapter.java similarity index 73% rename from src/main/java/subway/persistence/repository/H2StationRepository.java rename to src/main/java/subway/infrastructure/repository/StationPersistenceAdapter.java index 26be55632..fe222882f 100644 --- a/src/main/java/subway/persistence/repository/H2StationRepository.java +++ b/src/main/java/subway/infrastructure/repository/StationPersistenceAdapter.java @@ -1,20 +1,20 @@ -package subway.persistence.repository; +package subway.infrastructure.repository; import org.springframework.stereotype.Repository; -import subway.application.domain.Station; -import subway.application.repository.StationRepository; -import subway.persistence.dao.StationDao; -import subway.persistence.row.StationRow; +import subway.application.core.domain.Station; +import subway.application.port.StationRepository; +import subway.infrastructure.dao.StationDao; +import subway.infrastructure.entity.StationRow; import java.util.List; import java.util.stream.Collectors; @Repository -public class H2StationRepository implements StationRepository { +public class StationPersistenceAdapter implements StationRepository { private final StationDao stationDao; - public H2StationRepository(StationDao stationDao) { + public StationPersistenceAdapter(StationDao stationDao) { this.stationDao = stationDao; } diff --git a/src/main/java/subway/persistence/repository/H2LinePropertyRepository.java b/src/main/java/subway/persistence/repository/H2LinePropertyRepository.java deleted file mode 100644 index 8f3446305..000000000 --- a/src/main/java/subway/persistence/repository/H2LinePropertyRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package subway.persistence.repository; - -import org.springframework.stereotype.Repository; -import subway.application.domain.LineProperty; -import subway.application.repository.LinePropertyRepository; -import subway.persistence.dao.LinePropertyDao; -import subway.persistence.row.LinePropertyRow; - -import java.util.List; -import java.util.stream.Collectors; - -@Repository -public class H2LinePropertyRepository implements LinePropertyRepository { - - private final LinePropertyDao linePropertyDao; - - public H2LinePropertyRepository(LinePropertyDao linePropertyDao) { - this.linePropertyDao = linePropertyDao; - } - - public LineProperty insert(LineProperty lineProperty) { - LinePropertyRow row = linePropertyDao.insert( - new LinePropertyRow(lineProperty.getId(), lineProperty.getName(), lineProperty.getColor())); - - return new LineProperty(row.getId(), row.getName(), row.getColor()); - } - - public List findAll() { - List rows = linePropertyDao.selectAll(); - - return rows.stream() - .map(row -> new LineProperty(row.getId(), row.getName(), row.getColor())) - .collect(Collectors.toList()); - } - - public LineProperty findById(Long id) { - LinePropertyRow row = linePropertyDao.findById(id); - - return new LineProperty(row.getId(), row.getName(), row.getColor()); - } - - public void update(LineProperty lineProperty) { - linePropertyDao.update( - new LinePropertyRow(lineProperty.getId(), lineProperty.getName(), lineProperty.getColor())); - } - - public void deleteById(Long id) { - linePropertyDao.deleteById(id); - } -} diff --git a/src/main/java/subway/persistence/repository/H2LineRepository.java b/src/main/java/subway/persistence/repository/H2LineRepository.java deleted file mode 100644 index f31993914..000000000 --- a/src/main/java/subway/persistence/repository/H2LineRepository.java +++ /dev/null @@ -1,101 +0,0 @@ -package subway.persistence.repository; - -import org.springframework.stereotype.Repository; -import subway.application.repository.LineRepository; -import subway.application.domain.Distance; -import subway.application.domain.Line; -import subway.application.domain.LineProperty; -import subway.application.domain.Section; -import subway.application.domain.Station; -import subway.persistence.dao.LinePropertyDao; -import subway.persistence.dao.SectionDao; -import subway.persistence.dao.StationDao; -import subway.persistence.row.LinePropertyRow; -import subway.persistence.row.SectionRow; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -@Repository -public class H2LineRepository implements LineRepository { - - private final SectionDao sectionDao; - private final StationDao stationDao; - private final LinePropertyDao linePropertyDao; - - public H2LineRepository(SectionDao sectionDao, LinePropertyDao linePropertyDao, StationDao stationDao) { - this.sectionDao = sectionDao; - this.stationDao = stationDao; - this.linePropertyDao = linePropertyDao; - } - - public Line findById(Long linePropertyId) { - List sectionEntities = sectionDao.selectAllOfLinePropertyId(linePropertyId); - LinePropertyRow linePropertyRow = linePropertyDao.findById(linePropertyId); - - Map stationIds = getStationNameIdSet(sectionEntities); - List
sections = sectionEntities.stream() - .map(sectionRow -> new Section( - new Station(stationIds.get(sectionRow.getUpBound()), sectionRow.getUpBound()), - new Station(stationIds.get(sectionRow.getDownBound()), sectionRow.getDownBound()), - new Distance(sectionRow.getDistance()) - )).collect(Collectors.toList()); - - return new Line(new LineProperty(linePropertyRow.getId(), linePropertyRow.getName(), - linePropertyRow.getColor()), sections); - } - - private Map getStationNameIdSet(List sectionRows) { - if (sectionRows.isEmpty()) { - return Collections.emptyMap(); - } - return stationDao.selectKeyValueSetWhereNameIn(getStationNames(sectionRows)); - } - - private List getStationNames(List sectionEntities) { - Set stationNames = new HashSet<>(); - for (SectionRow sectionRow : sectionEntities) { - stationNames.add(sectionRow.getUpBound()); - stationNames.add(sectionRow.getDownBound()); - } - return new ArrayList<>(stationNames); - } - - public List findAll() { - List linePropertyRows = linePropertyDao.selectAll(); - List sectionRows = sectionDao.selectAll(); - Map stationIds = getStationNameIdSet(sectionRows); - - return linePropertyRows.stream() - .map(propertyRow -> new Line( - new LineProperty(propertyRow.getId(), propertyRow.getName(), propertyRow.getColor()), - findSectionsHasSamePropertyId(sectionRows, stationIds, propertyRow)) - ).collect(Collectors.toList()); - } - - private List
findSectionsHasSamePropertyId(List sectionRows, - Map stationIds, LinePropertyRow propertyRow) { - return sectionRows.stream() - .filter(sectionRow -> propertyRow.getId().equals(sectionRow.getLineId())) - .map(sectionRow -> new Section( - new Station(stationIds.get(sectionRow.getUpBound()), sectionRow.getUpBound()), - new Station(stationIds.get(sectionRow.getDownBound()), sectionRow.getDownBound()), - new Distance(sectionRow.getDistance()) - )).collect(Collectors.toList()); - } - - public void insert(Line line) { - List sectionRows = line.getSections().stream() - .map(section -> new SectionRow(null, line.getId(), section.getUpBound().getName(), - section.getDownBound().getName(), section.getDistance().value())) - .collect(Collectors.toList()); - - sectionDao.removeSections(line.getId()); - sectionDao.insertAll(sectionRows); - } -} diff --git a/src/main/java/subway/presentation/advice/ExceptionAdvice.java b/src/main/java/subway/presentation/ExceptionAdvice.java similarity index 78% rename from src/main/java/subway/presentation/advice/ExceptionAdvice.java rename to src/main/java/subway/presentation/ExceptionAdvice.java index 015becbc0..755bafa1a 100644 --- a/src/main/java/subway/presentation/advice/ExceptionAdvice.java +++ b/src/main/java/subway/presentation/ExceptionAdvice.java @@ -1,15 +1,16 @@ -package subway.presentation.advice; +package subway.presentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; -import org.springframework.dao.DuplicateKeyException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import subway.application.exception.ExpectedException; +import subway.application.core.exception.ExpectedException; import subway.presentation.dto.ExceptionResponse; +import javax.validation.ConstraintDeclarationException; +import javax.validation.ConstraintViolationException; import java.sql.SQLException; @RestControllerAdvice @@ -22,10 +23,10 @@ public ResponseEntity dataException(DataAccessException e) { logger.warn(e.getMessage(), e); return ResponseEntity.badRequest() - .build(); + .body(new ExceptionResponse("유효하지 않은 입력입니다.")); } - @ExceptionHandler(ExpectedException.class) + @ExceptionHandler({ExpectedException.class, ConstraintViolationException.class}) public ResponseEntity expectedException(ExpectedException e) { logger.warn(e.getMessage(), e); diff --git a/src/main/java/subway/presentation/LoggerInterceptor.java b/src/main/java/subway/presentation/LoggerInterceptor.java new file mode 100644 index 000000000..f7b7186c7 --- /dev/null +++ b/src/main/java/subway/presentation/LoggerInterceptor.java @@ -0,0 +1,32 @@ +package subway.presentation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +public class LoggerInterceptor implements HandlerInterceptor { + + private static final Logger logger = LoggerFactory.getLogger(LoggerInterceptor.class); + + private static final String REQUEST_LOG_FORMAT = "[REQUEST][%s][%s]"; + private static final String RESPONSE_LOG_FORMAT = "[RESPONSE][STATUS CODE : %d]"; + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, Object handler) { + logger.info(String.format(REQUEST_LOG_FORMAT, request.getMethod(), request.getRequestURI())); + return true; + } + + + @Override + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, Object handler, Exception ex) { + logger.info(String.format(RESPONSE_LOG_FORMAT, response.getStatus())); + } +} diff --git a/src/main/java/subway/presentation/controller/JourneyController.java b/src/main/java/subway/presentation/controller/JourneyController.java new file mode 100644 index 000000000..3d2462f12 --- /dev/null +++ b/src/main/java/subway/presentation/controller/JourneyController.java @@ -0,0 +1,54 @@ +package subway.presentation.controller; + +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.http.ResponseEntity; +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.application.core.domain.Station; +import subway.application.core.service.JourneyService; +import subway.application.core.service.dto.in.JourneyCommand; +import subway.application.core.service.dto.out.JourneyResult; +import subway.application.core.service.dto.out.StationResult; +import subway.presentation.dto.JourneyRequest; +import subway.presentation.dto.JourneyResponse; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/journey") +public class JourneyController { + + private final JourneyService journeyService; + + public JourneyController(JourneyService journeyService) { + this.journeyService = journeyService; + } + + @PostMapping + @Operation(summary = "make journey", description = "목적지까지의 최단경로/금액/거리 반환") + public ResponseEntity makeJourney(@RequestBody JourneyRequest request) { + JourneyResult result = journeyService.findShortestJourney( + new JourneyCommand(request.getDeparture(), request.getTerminal())); + JourneyResponse response = makeJourneyResponseFor(result); + + return ResponseEntity.ok(response); + } + + private JourneyResponse makeJourneyResponseFor(JourneyResult result) { + return new JourneyResponse( + collectStationNames(result), + result.getDistance(), + result.getFare() + ); + } + + private List collectStationNames(JourneyResult result) { + return result.getPath().stream() + .map(StationResult::getName) + .collect(Collectors.toList()); + } +} + diff --git a/src/main/java/subway/presentation/controller/LineController.java b/src/main/java/subway/presentation/controller/LineController.java index 5d3b80147..a8174b6f6 100644 --- a/src/main/java/subway/presentation/controller/LineController.java +++ b/src/main/java/subway/presentation/controller/LineController.java @@ -1,5 +1,6 @@ package subway.presentation.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -9,13 +10,18 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import subway.application.service.LineService; +import subway.application.core.service.LineService; +import subway.application.core.service.dto.in.DeleteStationCommand; +import subway.application.core.service.dto.in.EnrollStationCommand; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.out.StationResult; import subway.presentation.dto.StationEnrollRequest; import subway.presentation.dto.StationResponse; import java.net.URI; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @RestController @RequestMapping("/subway") @@ -28,26 +34,44 @@ public LineController(LineService lineService) { } @PostMapping("/{lineId}") + @Operation(summary = "enroll station", description = "노선에 역 추가") public ResponseEntity enrollStation(@PathVariable Long lineId, @RequestBody StationEnrollRequest request) { - lineService.enrollStation(lineId, request); + lineService.enrollStation( + new EnrollStationCommand(lineId, request.getUpBound(), request.getDownBound(), request.getDistance())); + return ResponseEntity.created(URI.create("/lines/" + lineId)).build(); } - @DeleteMapping("/{lineId}/{stationId}") + @DeleteMapping("/{lineId}/stations/{stationId}") + @Operation(summary = "delete station", description = "노선에서 역 제거") public ResponseEntity deleteStation(@PathVariable Long lineId, @PathVariable Long stationId) { - lineService.deleteStation(lineId, stationId); + lineService.deleteStation(new DeleteStationCommand(lineId, stationId)); + return ResponseEntity.status(HttpStatus.NO_CONTENT) .header("Location", "/line/" + lineId).build(); } @GetMapping("/{lineId}") + @Operation(summary = "get route map", description = "노선도 반환") public ResponseEntity> getRouteMap(@PathVariable Long lineId) { - return ResponseEntity.ok(lineService.findRouteMap(lineId)); + List results = lineService.findRouteMap(new IdCommand(lineId)); + List responses = results.stream() + .map(result -> new StationResponse(result.getId(), result.getName())) + .collect(Collectors.toList()); + + return ResponseEntity.ok(responses); } @GetMapping + @Operation(summary = "get all route maps", description = "모든 노선도 반환") public ResponseEntity>> getAllRouteMap() { - return ResponseEntity.ok(lineService.findAllRouteMap()); + Map> result = lineService.findAllRouteMap(); + Map> responses = result.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream() + .map(stationResult -> new StationResponse(stationResult.getId(), stationResult.getName())) + .collect(Collectors.toList()))); + + return ResponseEntity.ok(responses); } } diff --git a/src/main/java/subway/presentation/controller/LinePropertyController.java b/src/main/java/subway/presentation/controller/LinePropertyController.java index f01d55c34..4b8cf08d9 100644 --- a/src/main/java/subway/presentation/controller/LinePropertyController.java +++ b/src/main/java/subway/presentation/controller/LinePropertyController.java @@ -1,14 +1,19 @@ package subway.presentation.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import subway.application.service.LinePropertyService; +import subway.application.core.service.LinePropertyService; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.in.SaveLinePropertyCommand; +import subway.application.core.service.dto.in.UpdateLinePropertyCommand; +import subway.application.core.service.dto.out.LinePropertyResult; import subway.presentation.dto.LineRequest; -import subway.presentation.dto.LineResponse; +import subway.presentation.dto.LinePropertyResponse; import java.net.URI; -import java.sql.SQLException; import java.util.List; +import java.util.stream.Collectors; @RestController @RequestMapping("/lines") @@ -21,30 +26,49 @@ public LinePropertyController(LinePropertyService linePropertyService) { } @PostMapping - public ResponseEntity createLine(@RequestBody LineRequest lineRequest) { - LineResponse line = linePropertyService.saveLine(lineRequest); - return ResponseEntity.created(URI.create("/lines/" + line.getId())).body(line); + @Operation(summary = "create line property", description = "노선 정보 생성") + public ResponseEntity createLineProperty(@RequestBody LineRequest lineRequest) { + LinePropertyResult result = linePropertyService.saveLineProperty( + new SaveLinePropertyCommand(lineRequest.getName(), lineRequest.getColor())); + LinePropertyResponse response = new LinePropertyResponse(result.getId(), result.getName(), result.getColor()); + + return ResponseEntity.created(URI.create("/lines/" + response.getId())).body(response); } @GetMapping - public ResponseEntity> findAllLines() { - return ResponseEntity.ok(linePropertyService.findLineResponses()); + @Operation(summary = "find all line properties", description = "모든 노선 정보 반환") + public ResponseEntity> findAllLineProperties() { + List results = linePropertyService.findAllLineProperty(); + List responses = results.stream() + .map(result -> new LinePropertyResponse(result.getId(), result.getName(), result.getColor())) + .collect(Collectors.toList()); + + return ResponseEntity.ok(responses); } @GetMapping("/{id}") - public ResponseEntity findLineById(@PathVariable Long id) { - return ResponseEntity.ok(linePropertyService.findLineResponseById(id)); + @Operation(summary = "find line property by id", description = "노선 정보 반환") + public ResponseEntity findLinePropertyById(@PathVariable Long id) { + LinePropertyResult result = linePropertyService.findLinePropertyById(new IdCommand(id)); + LinePropertyResponse response = new LinePropertyResponse(result.getId(), result.getName(), result.getColor()); + + return ResponseEntity.ok(response); } @PutMapping("/{id}") - public ResponseEntity updateLine(@PathVariable Long id, @RequestBody LineRequest lineUpdateRequest) { - linePropertyService.updateLine(id, lineUpdateRequest); + @Operation(summary = "update line property by id", description = "노선 정보 수정") + public ResponseEntity updateLineProperty(@PathVariable Long id, @RequestBody LineRequest lineUpdateRequest) { + linePropertyService.updateLineProperty( + new UpdateLinePropertyCommand(id, lineUpdateRequest.getName(), lineUpdateRequest.getColor())); + return ResponseEntity.ok().build(); } @DeleteMapping("/{id}") - public ResponseEntity deleteLine(@PathVariable Long id) { - linePropertyService.deleteLineById(id); + @Operation(summary = "delete line property by id", description = "노선 정보 삭제") + public ResponseEntity deleteLineProperty(@PathVariable Long id) { + linePropertyService.deleteLinePropertyById(new IdCommand(id)); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/subway/presentation/controller/StationController.java b/src/main/java/subway/presentation/controller/StationController.java index 5908e8425..acb505170 100644 --- a/src/main/java/subway/presentation/controller/StationController.java +++ b/src/main/java/subway/presentation/controller/StationController.java @@ -1,14 +1,19 @@ package subway.presentation.controller; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.in.SaveStationCommand; +import subway.application.core.service.dto.in.UpdateStationCommand; +import subway.application.core.service.dto.out.StationResult; import subway.presentation.dto.StationRequest; import subway.presentation.dto.StationResponse; -import subway.application.service.StationService; +import subway.application.core.service.StationService; import java.net.URI; -import java.sql.SQLException; import java.util.List; +import java.util.stream.Collectors; @RestController @RequestMapping("/stations") @@ -21,30 +26,47 @@ public StationController(StationService stationService) { } @PostMapping + @Operation(summary = "create station", description = "역 정보 생성") public ResponseEntity createStation(@RequestBody StationRequest stationRequest) { - StationResponse station = stationService.saveStation(stationRequest); - return ResponseEntity.created(URI.create("/stations/" + station.getId())).body(station); + StationResult result = stationService.saveStation(new SaveStationCommand(stationRequest.getName())); + StationResponse response = new StationResponse(result.getId(), result.getName()); + + return ResponseEntity.created(URI.create("/stations/" + result.getId())).body(response); } @GetMapping + @Operation(summary = "show stations", description = "모든 역 정보 반환") public ResponseEntity> showStations() { - return ResponseEntity.ok().body(stationService.findAllStationResponses()); + List result = stationService.findAllStations(); + List responses = result.stream() + .map(stationResult -> new StationResponse(stationResult.getId(), stationResult.getName())) + .collect(Collectors.toList()); + + return ResponseEntity.ok().body(responses); } @GetMapping("/{id}") + @Operation(summary = "show station by id", description = "역 정보 반환") public ResponseEntity showStation(@PathVariable Long id) { - return ResponseEntity.ok().body(stationService.findStationResponseById(id)); + StationResult result = stationService.findStationById(new IdCommand(id)); + StationResponse response = new StationResponse(result.getId(), result.getName()); + + return ResponseEntity.ok().body(response); } @PutMapping("/{id}") + @Operation(summary = "update station by id", description = "역 정보 수정") public ResponseEntity updateStation(@PathVariable Long id, @RequestBody StationRequest stationRequest) { - stationService.updateStation(id, stationRequest); + stationService.updateStation(new UpdateStationCommand(id, stationRequest.getName())); + return ResponseEntity.ok().build(); } @DeleteMapping("/{id}") + @Operation(summary = "delete station by id", description = "역 정보 삭제") public ResponseEntity deleteStation(@PathVariable Long id) { - stationService.deleteStationById(id); + stationService.deleteStationById(new IdCommand(id)); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/subway/presentation/dto/JourneyRequest.java b/src/main/java/subway/presentation/dto/JourneyRequest.java new file mode 100644 index 000000000..cb85421d7 --- /dev/null +++ b/src/main/java/subway/presentation/dto/JourneyRequest.java @@ -0,0 +1,23 @@ +package subway.presentation.dto; + +public class JourneyRequest { + + private Long departure; + private Long terminal; + + public JourneyRequest() { + } + + public JourneyRequest(Long departure, Long terminal) { + this.departure = departure; + this.terminal = terminal; + } + + public Long getDeparture() { + return departure; + } + + public Long getTerminal() { + return terminal; + } +} diff --git a/src/main/java/subway/presentation/dto/JourneyResponse.java b/src/main/java/subway/presentation/dto/JourneyResponse.java new file mode 100644 index 000000000..f67f251f8 --- /dev/null +++ b/src/main/java/subway/presentation/dto/JourneyResponse.java @@ -0,0 +1,28 @@ +package subway.presentation.dto; + +import java.util.List; + +public class JourneyResponse { + + private final List path; + private final Double distance; + private final Integer fare; + + public JourneyResponse(List path, Double distance, Integer fare) { + this.path = path; + this.distance = distance; + this.fare = fare; + } + + public List getPath() { + return path; + } + + public Double getDistance() { + return distance; + } + + public Integer getFare() { + return fare; + } +} diff --git a/src/main/java/subway/presentation/dto/LineResponse.java b/src/main/java/subway/presentation/dto/LinePropertyResponse.java similarity index 54% rename from src/main/java/subway/presentation/dto/LineResponse.java rename to src/main/java/subway/presentation/dto/LinePropertyResponse.java index 0757e0d22..fe716af14 100644 --- a/src/main/java/subway/presentation/dto/LineResponse.java +++ b/src/main/java/subway/presentation/dto/LinePropertyResponse.java @@ -1,21 +1,21 @@ package subway.presentation.dto; -import subway.application.domain.LineProperty; +import subway.application.core.domain.LineProperty; -public class LineResponse { +public class LinePropertyResponse { private Long id; private String name; private String color; - public LineResponse(Long id, String name, String color) { + public LinePropertyResponse(Long id, String name, String color) { this.id = id; this.name = name; this.color = color; } - public static LineResponse of(LineProperty line) { - return new LineResponse(line.getId(), line.getName(), line.getColor()); + public static LinePropertyResponse of(LineProperty line) { + return new LinePropertyResponse(line.getId(), line.getName(), line.getColor()); } public Long getId() { diff --git a/src/main/java/subway/presentation/dto/StationResponse.java b/src/main/java/subway/presentation/dto/StationResponse.java index 516798618..a0f347e99 100644 --- a/src/main/java/subway/presentation/dto/StationResponse.java +++ b/src/main/java/subway/presentation/dto/StationResponse.java @@ -1,6 +1,6 @@ package subway.presentation.dto; -import subway.application.domain.Station; +import subway.application.core.domain.Station; public class StationResponse { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 119af0059..000000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,3 +0,0 @@ -spring.h2.console.enabled=true -spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL -spring.datasource.driver-class-name=org.h2.Driver diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..c39cce58a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,11 @@ +spring: + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:13306/subway?serverTimezone=UTC&characterEncoding=UTF-8 + username: user + password: password + + sql: + init: + mode: always diff --git a/src/main/resources/docker-compose.yml b/src/main/resources/docker-compose.yml new file mode 100644 index 000000000..34198ca13 --- /dev/null +++ b/src/main/resources/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.9" +services: + db: + image: mysql:8.0.28 + platform: linux/x86_64 + restart: always + ports: + - "13306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: subway + MYSQL_USER: user + MYSQL_PASSWORD: password + TZ: Asia/Seoul + volumes: + - ./db/mysql/data:/var/lib/mysql + - ./db/mysql/config:/etc/mysql/conf.d + - ./db/mysql/init:/docker-entrypoint-initdb.d diff --git a/src/main/resources/dummy.sql b/src/main/resources/dummy.sql new file mode 100644 index 000000000..3944223b9 --- /dev/null +++ b/src/main/resources/dummy.sql @@ -0,0 +1,37 @@ +DROP TABLE SECTION IF EXISTS; +DROP TABLE LINE IF EXISTS; +DROP TABLE STATION IF EXISTS; + +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) + ); + +CREATE TABLE IF NOT EXISTS SECTION +( + id BIGINT AUTO_INCREMENT NOT NULL, + line_id BIGINT NOT NULL, + up_bound BIGINT NOT NULL, + down_bound BIGINT NOT NULL, + distance INT NOT NULL, + PRIMARY KEY(ID), + FOREIGN KEY(up_bound) REFERENCES STATION(id) ON DELETE RESTRICT, + FOREIGN KEY(down_bound) REFERENCES STATION(id) ON DELETE RESTRICT + ); + +INSERT INTO STATION (name) values ('잠실역'); +INSERT INTO STATION (name) values ('방배역'); +INSERT INTO STATION (name) values ('서초역'); + +INSERT INTO LINE (name, color) values ('1호선', '파랑'); +INSERT INTO LINE (name, color) values ('2호선', '초록'); diff --git a/src/main/resources/logback-access.xml b/src/main/resources/logback-access.xml deleted file mode 100644 index 38e0823f4..000000000 --- a/src/main/resources/logback-access.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - %n###### HTTP Request ######%n%fullRequest%n###### HTTP Response ######%n%fullResponse%n%n - - - - \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 4c81a2237..40c3afdc7 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS STATION PRIMARY KEY(ID) ); -CREATE TABLE IF NOT EXISTS LINE_PROPERTY +CREATE TABLE IF NOT EXISTS LINE ( id BIGINT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL UNIQUE, @@ -15,10 +15,12 @@ CREATE TABLE IF NOT EXISTS LINE_PROPERTY CREATE TABLE IF NOT EXISTS SECTION ( - id BIGINT AUTO_INCREMENT NOT NULL, - line_id BIGINT NOT NULL, - up_bound VARCHAR(255) NOT NULL, - down_bound VARCHAR(255) NOT NULL, - distance INT NOT NULL, - PRIMARY KEY(ID) + id BIGINT AUTO_INCREMENT NOT NULL, + line_id BIGINT NOT NULL, + up_bound BIGINT NOT NULL, + down_bound BIGINT NOT NULL, + distance INT NOT NULL, + PRIMARY KEY(ID), + FOREIGN KEY(up_bound) REFERENCES STATION(id) ON DELETE RESTRICT, + FOREIGN KEY(down_bound) REFERENCES STATION(id) ON DELETE RESTRICT ); diff --git a/src/test/java/subway/StationFixture.java b/src/test/java/subway/StationFixture.java index ae364b89d..12a86f61f 100644 --- a/src/test/java/subway/StationFixture.java +++ b/src/test/java/subway/StationFixture.java @@ -1,6 +1,6 @@ package subway; -import subway.application.domain.Station; +import subway.application.core.domain.Station; public class StationFixture { diff --git a/src/test/java/subway/SubwayApplicationTests.java b/src/test/java/subway/SubwayApplicationTests.java index cdf84476f..7461fb971 100644 --- a/src/test/java/subway/SubwayApplicationTests.java +++ b/src/test/java/subway/SubwayApplicationTests.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class SubwayApplicationTests { @Test diff --git a/src/test/java/subway/domain/DistanceTest.java b/src/test/java/subway/application/core/domain/DistanceTest.java similarity index 73% rename from src/test/java/subway/domain/DistanceTest.java rename to src/test/java/subway/application/core/domain/DistanceTest.java index bc0b1cd23..2403edfba 100644 --- a/src/test/java/subway/domain/DistanceTest.java +++ b/src/test/java/subway/application/core/domain/DistanceTest.java @@ -1,10 +1,9 @@ -package subway.domain; +package subway.application.core.domain; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import subway.application.domain.Distance; -import subway.application.exception.DistanceExceedException; +import subway.application.core.exception.DistanceNotPositiveException; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -16,6 +15,6 @@ class DistanceTest { void constructor(int distance) { //given, when, then assertThatThrownBy(() -> new Distance(distance)) - .isInstanceOf(DistanceExceedException.class); + .isInstanceOf(DistanceNotPositiveException.class); } } diff --git a/src/test/java/subway/application/core/domain/FareTest.java b/src/test/java/subway/application/core/domain/FareTest.java new file mode 100644 index 000000000..f298f14ef --- /dev/null +++ b/src/test/java/subway/application/core/domain/FareTest.java @@ -0,0 +1,65 @@ +package subway.application.core.domain; + +import org.junit.jupiter.api.DisplayName; +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.application.core.exception.FareCantCalculatedException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FareTest { + + @Test + @DisplayName("거리는 0KM 미만일 수 없다") + void distance_negative_exception() { + // given + assertThatThrownBy(() -> Fare.of(-1)) + .isInstanceOf(FareCantCalculatedException.class); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, 5.0}) + @DisplayName("거리는 0KM 이상이라면 정상 생성된다") + void distance_normal(double distance) { + // given + assertThatCode(() -> Fare.of(distance)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, 4.0, 9.0}) + @DisplayName("거리가 10KM 이내라면 기본운임이 부과된다") + void charge_default(double distance) { + // given + int fare = Fare.of(distance); + + // when, then + assertThat(fare).isEqualTo(1_250); + } + + @ParameterizedTest + @CsvSource(value = {"9:1250", "12:1350", "16:1450", "50:2050"}, delimiter = ':') + @DisplayName("거리가 10KM 이상, 50KM 이하라면 5KM마다 100원이 추가된다") + void charge_betweenTenAndFifty(double distance, int expectedValue) { + // given + int fare = Fare.of(distance); + + // when, then + assertThat(fare).isEqualTo(expectedValue); + } + + @ParameterizedTest + @CsvSource(value = {"51:2150", "58:2150", "59:2250"}, delimiter = ':') + @DisplayName("거리가 50KM보다 크다면 8KM마다 100원이 추가된다") + void charge_overFifty(double distance, int expectedValue) { + // given + int fare = Fare.of(distance); + + // when, then + assertThat(fare).isEqualTo(expectedValue); + } +} diff --git a/src/test/java/subway/domain/LineTest.java b/src/test/java/subway/application/core/domain/LineTest.java similarity index 92% rename from src/test/java/subway/domain/LineTest.java rename to src/test/java/subway/application/core/domain/LineTest.java index 086ea1843..a2fe1a10c 100644 --- a/src/test/java/subway/domain/LineTest.java +++ b/src/test/java/subway/application/core/domain/LineTest.java @@ -1,18 +1,18 @@ -package subway.domain; +package subway.application.core.domain; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import subway.StationFixture; -import subway.application.domain.Distance; -import subway.application.domain.Line; -import subway.application.domain.LineProperty; -import subway.application.domain.Section; -import subway.application.domain.Station; -import subway.application.exception.StationAlreadyExistsException; -import subway.application.exception.StationConnectException; -import subway.application.exception.StationNotExistsException; -import subway.application.exception.StationTooFarException; +import subway.application.core.domain.Distance; +import subway.application.core.domain.Line; +import subway.application.core.domain.LineProperty; +import subway.application.core.domain.Section; +import subway.application.core.domain.Station; +import subway.application.core.exception.StationAlreadyExistsException; +import subway.application.core.exception.StationConnectException; +import subway.application.core.exception.StationNotExistsException; +import subway.application.core.exception.StationTooFarException; import java.util.ArrayList; diff --git a/src/test/java/subway/domain/RouteMapTest.java b/src/test/java/subway/application/core/domain/RouteMapTest.java similarity index 83% rename from src/test/java/subway/domain/RouteMapTest.java rename to src/test/java/subway/application/core/domain/RouteMapTest.java index cb42f5fc4..24e271242 100644 --- a/src/test/java/subway/domain/RouteMapTest.java +++ b/src/test/java/subway/application/core/domain/RouteMapTest.java @@ -1,14 +1,14 @@ -package subway.domain; +package subway.application.core.domain; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import subway.StationFixture; -import subway.application.domain.Distance; -import subway.application.domain.RouteMap; -import subway.application.domain.Section; -import subway.application.domain.Station; -import subway.application.exception.CircularRouteException; -import subway.application.exception.RouteNotConnectedException; +import subway.application.core.domain.Distance; +import subway.application.core.domain.RouteMap; +import subway.application.core.domain.Section; +import subway.application.core.domain.Station; +import subway.application.core.exception.CircularRouteException; +import subway.application.core.exception.RouteNotConnectedException; import java.util.Collections; import java.util.List; @@ -26,7 +26,7 @@ void getRouteMap_emptyCase() { RouteMap routeMap = new RouteMap(sections); //when - List stations = routeMap.value(); + List stations = routeMap.stations(); //then assertThat(stations).isEqualTo(Collections.emptyList()); @@ -71,7 +71,7 @@ void getRouteMap() { ); //when - List routeMap = new RouteMap(sections).value(); + List routeMap = new RouteMap(sections).stations(); //then assertThat(routeMap).containsExactly( diff --git a/src/test/java/subway/application/core/service/JourneyServiceTest.java b/src/test/java/subway/application/core/service/JourneyServiceTest.java new file mode 100644 index 000000000..ce238f6dd --- /dev/null +++ b/src/test/java/subway/application/core/service/JourneyServiceTest.java @@ -0,0 +1,44 @@ +package subway.application.core.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; +import subway.StationFixture; +import subway.application.core.service.dto.in.EnrollStationCommand; +import subway.application.core.service.dto.in.JourneyCommand; +import subway.application.core.service.dto.out.JourneyResult; +import subway.application.core.service.dto.out.StationResult; + +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql("classpath:dummy.sql") +class JourneyServiceTest { + + @Autowired + private JourneyService journeyService; + @Autowired + private LineService lineService; + + @Test + @DisplayName("출발지, 종착지가 주어지면 최단 경로를 반환할 수 있다") + void findShortestJourney() { + // given + lineService.enrollStation(new EnrollStationCommand(1L, 1L, 2L, 3)); + lineService.enrollStation(new EnrollStationCommand(1L, 2L, 3L, 1)); + + JourneyCommand command = new JourneyCommand(1L, 3L); + + // when + JourneyResult result = journeyService.findShortestJourney(command); + + // then + assertThat(result.getPath()).hasSize(3); + assertThat(result.getDistance()).isEqualTo(4); + assertThat(result.getFare()).isEqualTo(1_250); + } +} diff --git a/src/test/java/subway/application/core/service/LinePropertyServiceTest.java b/src/test/java/subway/application/core/service/LinePropertyServiceTest.java new file mode 100644 index 000000000..89e22fa2a --- /dev/null +++ b/src/test/java/subway/application/core/service/LinePropertyServiceTest.java @@ -0,0 +1,103 @@ +package subway.application.core.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.in.SaveLinePropertyCommand; +import subway.application.core.service.dto.in.UpdateLinePropertyCommand; +import subway.application.core.service.dto.out.LinePropertyResult; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class LinePropertyServiceTest { + + @Autowired + private LinePropertyService linePropertyService; + + @Test + @DisplayName("노선 정보를 저장할 수 있다") + void saveLineProperty() { + // given + SaveLinePropertyCommand command = new SaveLinePropertyCommand("1호선", "파랑"); + LinePropertyResult saved = linePropertyService.saveLineProperty(command); + + // when + IdCommand idCommand = new IdCommand(saved.getId()); + LinePropertyResult actual = linePropertyService.findLinePropertyById(idCommand); + + // then + assertThat(saved).usingRecursiveComparison().isEqualTo(actual); + } + + @Test + @DisplayName("노선 정보를 모두 찾을 수 있다") + void findLinePropertyResponses() { + // given + SaveLinePropertyCommand command = new SaveLinePropertyCommand("1호선", "파랑"); + LinePropertyResult saved = linePropertyService.saveLineProperty(command); + + // when + List results = linePropertyService.findAllLineProperty(); + LinePropertyResult expected = new LinePropertyResult(saved.getId(), "1호선", "파랑"); + + // then + assertThat(results.get(0)).usingRecursiveComparison() + .ignoringExpectedNullFields().isEqualTo(expected); + } + + @Test + @DisplayName("특정 노선 정보를 찾을 수 있다") + void findLinePropertyResponseById() { + // given + SaveLinePropertyCommand command = new SaveLinePropertyCommand("1호선", "파랑"); + LinePropertyResult saved = linePropertyService.saveLineProperty(command); + + // when + IdCommand idCommand = new IdCommand(saved.getId()); + LinePropertyResult result = linePropertyService.findLinePropertyById(idCommand); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(saved); + } + + @Test + @DisplayName("특정 노선을 업데이트 할 수 있다") + void updateLineProperty() { + // given + SaveLinePropertyCommand command = new SaveLinePropertyCommand("1호선", "파랑"); + LinePropertyResult saved = linePropertyService.saveLineProperty(command); + + // when + UpdateLinePropertyCommand updateCommand = + new UpdateLinePropertyCommand(saved.getId(), "1호선", "빨강"); + linePropertyService.updateLineProperty(updateCommand); + LinePropertyResult findResult = linePropertyService.findLinePropertyById( + new IdCommand(saved.getId())); + LinePropertyResult expected = new LinePropertyResult(saved.getId(), "1호선", "빨강"); + + // then + assertThat(findResult).usingRecursiveComparison().isEqualTo(expected); + } + + @Test + @DisplayName("특정 노선을 삭제할 수 있다") + void deleteLinePropertyById() { + // given + SaveLinePropertyCommand command = new SaveLinePropertyCommand("1호선", "파랑"); + LinePropertyResult saved = linePropertyService.saveLineProperty(command); + + // when + IdCommand idCommand = new IdCommand(saved.getId()); + linePropertyService.deleteLinePropertyById(idCommand); + + // then + assertThat(linePropertyService.findAllLineProperty()).isEmpty(); + } +} diff --git a/src/test/java/subway/application/core/service/LineServiceTest.java b/src/test/java/subway/application/core/service/LineServiceTest.java new file mode 100644 index 000000000..59dd6d173 --- /dev/null +++ b/src/test/java/subway/application/core/service/LineServiceTest.java @@ -0,0 +1,85 @@ +package subway.application.core.service; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; +import subway.application.core.service.dto.in.DeleteStationCommand; +import subway.application.core.service.dto.in.EnrollStationCommand; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.out.StationResult; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql("classpath:dummy.sql") +class LineServiceTest { + + @Autowired + private LineService lineService; + + @Test + @DisplayName("역을 노선에 등록할 수 있다") + void enrollStation() { + // given + EnrollStationCommand command = new EnrollStationCommand(1L, 1L, 2L, 3); + + // when + lineService.enrollStation(command); + + // then + assertThat(lineService.findAllRouteMap()).isNotEmpty(); + } + + @Test + @DisplayName("역을 노선에서 삭제할 수 있다") + void deleteStation() { + // given + lineService.enrollStation(new EnrollStationCommand(1L, 1L, 2L, 3)); + DeleteStationCommand command = new DeleteStationCommand(1L, 1L); + + // when + lineService.deleteStation(command); + + // then + assertThat(lineService.findAllRouteMap().get("1호선")).hasSize(0); + } + + @Test + @DisplayName("노선을 찾을 수 있다") + void findRouteMap() { + // given + lineService.enrollStation(new EnrollStationCommand(1L, 1L, 2L, 3)); + + // when + IdCommand idCommand = new IdCommand(1L); + List routeMap = lineService.findRouteMap(idCommand); + + // then + assertThat(routeMap.get(0).getName()).isEqualTo("잠실역"); + assertThat(routeMap.get(1).getName()).isEqualTo("방배역"); + } + + @Test + @DisplayName("모든 노선을 찾을 수 있다") + void findAllRouteMap() { + // given + lineService.enrollStation(new EnrollStationCommand(1L, 1L, 2L, 3)); + lineService.enrollStation(new EnrollStationCommand(2L, 2L, 1L, 3)); + + // when + Map> allRouteMap = lineService.findAllRouteMap(); + + // then + assertThat(allRouteMap.get("1호선")).hasSize(2); + assertThat(allRouteMap.get("2호선")).hasSize(2); + } +} diff --git a/src/test/java/subway/application/core/service/StationServiceTest.java b/src/test/java/subway/application/core/service/StationServiceTest.java new file mode 100644 index 000000000..7c57a451c --- /dev/null +++ b/src/test/java/subway/application/core/service/StationServiceTest.java @@ -0,0 +1,105 @@ +package subway.application.core.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import subway.application.core.service.dto.in.IdCommand; +import subway.application.core.service.dto.in.SaveStationCommand; +import subway.application.core.service.dto.in.UpdateStationCommand; +import subway.application.core.service.dto.out.StationResult; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class StationServiceTest { + + @Autowired + private StationService stationService; + + @Test + @DisplayName("역을 저장할 수 있다") + void saveStation() { + // given + SaveStationCommand saveCommand = new SaveStationCommand("잠실역"); + + // when + StationResult result = stationService.saveStation(saveCommand); + StationResult expected = new StationResult(null, "잠실역"); + + // then + assertThat(result).usingRecursiveComparison() + .ignoringExpectedNullFields().isEqualTo(expected); + } + + @Test + @DisplayName("ID를 통해 역을 찾을 수 있다") + void findStationResponseById() { + // given + SaveStationCommand saveCommand = new SaveStationCommand("잠실역"); + StationResult saved = stationService.saveStation(saveCommand); + + // when + IdCommand idCommand = new IdCommand(saved.getId()); + StationResult findResult = stationService.findStationById(idCommand); + + // then + assertThat(findResult).usingRecursiveComparison() + .ignoringExpectedNullFields().isEqualTo(saved); + } + + @Test + @DisplayName("모든 역을 찾을 수 있다") + void findAllStationResponses() { + // given + SaveStationCommand saveCommand = new SaveStationCommand("잠실역"); + StationResult saved = stationService.saveStation(saveCommand); + + // when + List findResults = stationService.findAllStations(); + + // then + assertThat(findResults.get(0)).usingRecursiveComparison() + .ignoringExpectedNullFields().isEqualTo(saved); + } + + @Test + @DisplayName("역을 업데이트 할 수 있다") + void updateStation() { + // given + SaveStationCommand saveCommand = new SaveStationCommand("잠실역"); + StationResult saved = stationService.saveStation(saveCommand); + + // when + UpdateStationCommand updateCommand = new UpdateStationCommand(saved.getId(), "방배역"); + stationService.updateStation(updateCommand); + + StationResult actual = stationService.findStationById(new IdCommand(saved.getId())); + StationResult expected = new StationResult(saved.getId(), "방배역"); + + // then + assertThat(actual).usingRecursiveComparison() + .ignoringExpectedNullFields().isEqualTo(expected); + } + + @Test + @DisplayName("역을 삭제할 수 있다") + void deleteStationById() { + // given + SaveStationCommand saveCommand = new SaveStationCommand("잠실역"); + StationResult saved = stationService.saveStation(saveCommand); + + // when + IdCommand idCommand = new IdCommand(saved.getId()); + stationService.deleteStationById(idCommand); + + List actual = stationService.findAllStations(); + + // then + assertThat(actual).isEmpty(); + } +} diff --git a/src/test/java/subway/infrastructure/graph/JgraphtPathFinderTest.java b/src/test/java/subway/infrastructure/graph/JgraphtPathFinderTest.java new file mode 100644 index 000000000..f7e64e929 --- /dev/null +++ b/src/test/java/subway/infrastructure/graph/JgraphtPathFinderTest.java @@ -0,0 +1,130 @@ +package subway.infrastructure.graph; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import subway.StationFixture; +import subway.application.core.domain.Distance; +import subway.application.core.domain.RouteMap; +import subway.application.core.domain.Section; +import subway.application.core.domain.Station; +import subway.application.core.service.dto.out.PathFindResult; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class JgraphtPathFinderTest { + + private static JgraphtPathFinder jgraphtPathFinder; + + @BeforeAll + static void setup() { + jgraphtPathFinder = new JgraphtPathFinder(); + } + + @Test + @DisplayName("그래프가 일직선으로 이어진 경우, 최단경로를 및 거리를 찾을 수 있다") + void findShortestPath_oneLine() { + // given + List routeMaps = List.of( + routeMapOf(List.of( + StationFixture.of(1L, "A"), + StationFixture.of(2L, "B"), + StationFixture.of(3L, "C"), + StationFixture.of(4L, "D") + )) + ); + Station departure = new Station(1L, "A"); + Station terminal = new Station(4L, "D"); + + // when + PathFindResult result = jgraphtPathFinder.findShortestPath(routeMaps, departure, terminal); + + // then + assertThat(result.getShortestPath()).containsExactly( + StationFixture.of(1L, "A"), StationFixture.of(2L, "B"), + StationFixture.of(3L, "C"), StationFixture.of(4L, "D") + ); + assertThat(result.getDistance()).isEqualTo(3); + } + + @Test + @DisplayName("환승할 수 있는 경우, 최단경로를 및 거리를 찾을 수 있다") + void findShortestPath_transfer() { + // given + List routeMaps = List.of( + routeMapOf(List.of( + StationFixture.of(1L, "A"), + StationFixture.of(2L, "B"), + StationFixture.of(3L, "C"), + StationFixture.of(4L, "D") + )), + routeMapOf(List.of( + StationFixture.of(2L, "B"), + StationFixture.of(5L, "E"), + StationFixture.of(6L, "F") + )) + ); + Station departure = new Station(1L, "A"); + Station terminal = new Station(6L, "F"); + + // when + PathFindResult result = jgraphtPathFinder.findShortestPath(routeMaps, departure, terminal); + + // then + assertThat(result.getShortestPath()).containsExactly( + StationFixture.of(1L, "A"), StationFixture.of(2L, "B"), + StationFixture.of(5L, "E"), StationFixture.of(6L, "F") + ); + assertThat(result.getDistance()).isEqualTo(3); + } + + @Test + @DisplayName("해당 목적지로 가는 여러 경로가 존재하는 경우, 최단경로를 및 거리를 찾을 수 있다") + void findShortestPath_selectRoute() { + // given + List routeMaps = List.of( + routeMapOf(List.of( + StationFixture.of(1L, "A"), + StationFixture.of(2L, "B"), + StationFixture.of(3L, "C") + )), + routeMapOf(List.of( + StationFixture.of(1L, "A"), + StationFixture.of(4L, "D") + )), + routeMapOf(List.of( + StationFixture.of(3L, "C"), + StationFixture.of(5L, "E"), + StationFixture.of(4L, "D") + )) + ); + Station departure = new Station(1L, "A"); + Station terminal = new Station(5L, "E"); + + // when + PathFindResult result = jgraphtPathFinder.findShortestPath(routeMaps, departure, terminal); + + // then + assertThat(result.getShortestPath()).containsExactly( + StationFixture.of(1L, "A"), StationFixture.of(4L, "D"), + StationFixture.of(5L, "E") + ); + assertThat(result.getDistance()).isEqualTo(2); + } + + RouteMap routeMapOf(List stations) { + List
sections = new ArrayList<>(); + for (int i = 0; i < stations.size() - 1; i++) { + sections.add(new Section( + stations.get(i), + stations.get(i + 1), + new Distance(1) + )); + } + return new RouteMap(sections); + } +} diff --git a/src/test/java/subway/infrastructure/repository/LinePersistenceAdapterTest.java b/src/test/java/subway/infrastructure/repository/LinePersistenceAdapterTest.java new file mode 100644 index 000000000..b1dba1411 --- /dev/null +++ b/src/test/java/subway/infrastructure/repository/LinePersistenceAdapterTest.java @@ -0,0 +1,81 @@ +package subway.infrastructure.repository; + +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.application.core.domain.Line; +import subway.application.core.domain.LineProperty; +import subway.infrastructure.dao.LineDao; +import subway.infrastructure.dao.SectionDao; +import subway.infrastructure.dao.StationDao; +import subway.infrastructure.entity.LineRow; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LinePersistenceAdapterTest { + + @InjectMocks + private LinePersistenceAdapter lineRepository; + @Mock + private SectionDao sectionDao; + @Mock + private StationDao stationDao; + @Mock + private LineDao lineDao; + + @Test + @DisplayName("ID를 통해 Line을 찾을 수 있다") + void findById() { + // given + when(lineDao.findById(any())).thenReturn(new LineRow(1L, "1호선", "파랑")); + + // when + Line found = lineRepository.findById(1L); + + // then + assertThat(found).isNotNull(); + } + + @Test + @DisplayName("모든 Line을 찾을 수 있다") + void findAll() { + // given + when(lineDao.selectAll()).thenReturn(List.of( + new LineRow(1L, "1호선", "파랑"), + new LineRow(2L, "2호선", "초록") + )); + when(sectionDao.selectAll()).thenReturn(Collections.emptyList()); + + // when + List found = lineRepository.findAll(); + + // then + assertThat(found).hasSize(2); + } + + @Test + @DisplayName("Line을 삽입할 수 있다") + void insert() { + // given + LineRow lineRow = new LineRow(1L, "1호선", "파랑"); + when(lineDao.findById(any())).thenReturn(lineRow); + + // when + Line inserted = lineRepository.insert(new Line( + new LineProperty(1L, "1호선", "파랑"), + Collections.emptyList() + )); + + // then + assertThat(inserted.getId()).isEqualTo(lineRow.getId()); + } +} diff --git a/src/test/java/subway/infrastructure/repository/LinePropertyPersistenceAdapterTest.java b/src/test/java/subway/infrastructure/repository/LinePropertyPersistenceAdapterTest.java new file mode 100644 index 000000000..8c9fd0ae2 --- /dev/null +++ b/src/test/java/subway/infrastructure/repository/LinePropertyPersistenceAdapterTest.java @@ -0,0 +1,96 @@ +package subway.infrastructure.repository; + +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.application.core.domain.LineProperty; +import subway.infrastructure.dao.LineDao; + +import javax.sql.DataSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +class LinePropertyPersistenceAdapterTest { + + private LinePropertyPersistenceAdapter linePropertyRepository; + + @Autowired + public LinePropertyPersistenceAdapterTest(JdbcTemplate jdbcTemplate, DataSource dataSource) { + LineDao lineDao = new LineDao(jdbcTemplate, dataSource); + this.linePropertyRepository = new LinePropertyPersistenceAdapter(lineDao); + } + + @Test + @DisplayName("LineProperty를 삽입할 수 있다") + void insert() { + // given + LineProperty lineProperty = new LineProperty(null, "1호선", "파랑"); + + // when + LineProperty inserted = linePropertyRepository.insert(lineProperty); + + // then + assertThat(inserted).usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(lineProperty); + } + + @Test + @DisplayName("LineProperty를 모두 찾을 수 있다") + void findAll() { + // given + linePropertyRepository.insert(new LineProperty(null, "1호선", "파랑")); + linePropertyRepository.insert(new LineProperty(null, "2호선", "초록")); + + // when + List found = linePropertyRepository.findAll(); + + // then + assertThat(found).hasSize(2); + } + + @Test + @DisplayName("LineProperty를 ID를 통해 찾을 수 있다") + void findById() { + // given + LineProperty inserted = linePropertyRepository.insert(new LineProperty(null, "1호선", "파랑")); + + // when + LineProperty found = linePropertyRepository.findById(inserted.getId()); + + // then + assertThat(found).usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(inserted); + } + + @Test + @DisplayName("LineProperty를 업데이트 할 수 있다") + void update() { + // given + LineProperty inserted = linePropertyRepository.insert(new LineProperty(null, "1호선", "파랑")); + + // when + LineProperty updated = new LineProperty(inserted.getId(), "2호선", "초록"); + linePropertyRepository.update(updated); + LineProperty found = linePropertyRepository.findById(inserted.getId()); + + // then + assertThat(found).usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(updated); + } + + @Test + @DisplayName("LineProperty를 삭제할 수 있다") + void deleteById() { + // given + LineProperty inserted = linePropertyRepository.insert(new LineProperty(null, "1호선", "파랑")); + + // when + linePropertyRepository.deleteById(inserted.getId()); + List found = linePropertyRepository.findAll(); + + // then + assertThat(found).isEmpty(); + } +} diff --git a/src/test/java/subway/infrastructure/repository/StationPersistenceAdapterTest.java b/src/test/java/subway/infrastructure/repository/StationPersistenceAdapterTest.java new file mode 100644 index 000000000..0f508fca2 --- /dev/null +++ b/src/test/java/subway/infrastructure/repository/StationPersistenceAdapterTest.java @@ -0,0 +1,96 @@ +package subway.infrastructure.repository; + +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.application.core.domain.Station; +import subway.infrastructure.dao.StationDao; + +import javax.sql.DataSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +class StationPersistenceAdapterTest { + + private StationPersistenceAdapter stationRepository; + + @Autowired + public StationPersistenceAdapterTest(JdbcTemplate jdbcTemplate, DataSource dataSource) { + StationDao stationDao = new StationDao(jdbcTemplate, dataSource); + stationRepository = new StationPersistenceAdapter(stationDao); + } + + @Test + @DisplayName("Station을 삽입할 수 있다") + void insert() { + // given + Station station = new Station(null, "잠실역"); + + // when + Station inserted = stationRepository.insert(station); + + // then + assertThat(inserted).usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(station); + } + + @Test + @DisplayName("Station을 모두 찾을 수 있다") + void findAll() { + // given + stationRepository.insert(new Station(null, "잠실역")); + stationRepository.insert(new Station(null, "방배역")); + + // when + List found = stationRepository.findAll(); + + // then + assertThat(found).hasSize(2); + } + + @Test + @DisplayName("Station을 ID를 통해 찾을 수 있다") + void findById() { + // given + Station inserted = stationRepository.insert(new Station(null, "잠실역")); + + // when + Station found = stationRepository.findById(inserted.getId()); + + // then + assertThat(found).usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(inserted); + } + + @Test + @DisplayName("Station을 업데이트 할 수 있다") + void update() { + // given + Station inserted = stationRepository.insert(new Station(null, "잠실역")); + + // when + Station updated = new Station(inserted.getId(), "방배역"); + stationRepository.update(updated); + Station found = stationRepository.findById(inserted.getId()); + + // then + assertThat(found).usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(updated); + } + + @Test + @DisplayName("Station을 삭제할 수 있다") + void deleteById() { + // given + Station inserted = stationRepository.insert(new Station(null, "잠실역")); + + // when + stationRepository.deleteById(inserted.getId()); + List found = stationRepository.findAll(); + + // then + assertThat(found).isEmpty(); + } +} diff --git a/src/test/java/subway/integration/IntegrationTest.java b/src/test/java/subway/integration/IntegrationTest.java index 8e7958370..3f931afbd 100644 --- a/src/test/java/subway/integration/IntegrationTest.java +++ b/src/test/java/subway/integration/IntegrationTest.java @@ -2,15 +2,12 @@ import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.annotation.DirtiesContext; @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) public class IntegrationTest { @LocalServerPort diff --git a/src/test/java/subway/integration/JourneyIntegrationTest.java b/src/test/java/subway/integration/JourneyIntegrationTest.java new file mode 100644 index 000000000..061c4e42d --- /dev/null +++ b/src/test/java/subway/integration/JourneyIntegrationTest.java @@ -0,0 +1,92 @@ +package subway.integration; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import subway.presentation.dto.JourneyRequest; +import subway.presentation.dto.JourneyResponse; +import subway.presentation.dto.LineRequest; +import subway.presentation.dto.StationEnrollRequest; +import subway.presentation.dto.StationRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("최단 경로 관련 기능") +public class JourneyIntegrationTest extends IntegrationTest { + + @Test + @DisplayName("최단 경로를 찾을 수 있다") + void test_shortestPath() { + // given + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new StationRequest("잠실역")) + + .when().post("/stations"); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new StationRequest("방배역")) + + .when().post("/stations"); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new StationRequest("서초역")) + + .when().post("/stations"); + + long blueLineId = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new LineRequest("1호선", "파랑")) + + .when().post("/lines") + + .then() + .extract() + .jsonPath() + .getLong("id"); + + long greenLineId = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new LineRequest("2호선", "초록")) + + .when().post("/lines") + + .then() + .extract() + .jsonPath() + .getLong("id"); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new StationEnrollRequest(1L, 2L, 3)) + + .when().post("/subway/{lineId}", blueLineId); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new StationEnrollRequest(2L, 3L, 4)) + + .when().post("/subway/{lineId}", greenLineId); + + // when + JourneyResponse journeyResponse = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new JourneyRequest(1L, 3L)) + + .when().post("/journey") + + .then() + .extract() + .body() + .jsonPath() + .getObject(".", JourneyResponse.class); + + // then + assertThat(journeyResponse.getDistance()).isEqualTo(7); + assertThat(journeyResponse.getPath()).hasSize(3); + assertThat(journeyResponse.getFare()).isEqualTo(1_250); + } +} diff --git a/src/test/java/subway/integration/LineIntegrationTest.java b/src/test/java/subway/integration/LineIntegrationTest.java index 58bd2cf75..b8f430f75 100644 --- a/src/test/java/subway/integration/LineIntegrationTest.java +++ b/src/test/java/subway/integration/LineIntegrationTest.java @@ -1,190 +1,268 @@ 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.presentation.dto.LineRequest; -import subway.presentation.dto.LineResponse; +import subway.presentation.dto.StationEnrollRequest; +import subway.presentation.dto.StationRequest; +import subway.presentation.dto.StationResponse; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertAll; -@DisplayName("지하철 노선 관련 기능") +@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() + @DisplayName("지하철 노선에 역을 추가할 수 있다") + void test_addStationToLine() { + //given + LineRequest lineRequest = new LineRequest("2호선", "초록"); + + long lineId = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) + .body(lineRequest) + .when().post("/lines") - .then().log().all(). - extract(); - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.header("Location")).isNotBlank(); - } + .then() + .extract() + .jsonPath() + .getLong("id"); - @DisplayName("기존에 존재하는 지하철 노선 이름으로 지하철 노선을 생성한다.") - @Test - void createLineWithDuplicateName() { - // given - RestAssured - .given().log().all() + StationRequest jamsilRequest = new StationRequest("잠실역"); + StationRequest bangbaeRequest = new StationRequest("방배역"); + + long jamsilStationId = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); + .body(jamsilRequest) + + .when().post("/stations") - // when - ExtractableResponse response = RestAssured - .given().log().all() + .then() + .extract() + .jsonPath() + .getLong("id"); + + long bangbaeStationId = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); + .body(bangbaeRequest) + + .when().post("/stations") + + .then() + .extract() + .jsonPath().getLong("id"); + + StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( + jamsilStationId, bangbaeStationId, 3); - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + //when, then + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(stationEnrollRequest) + + .when().post("/subway/{lineId}", lineId) + + .then() + .statusCode(HttpStatus.CREATED.value()); } - @DisplayName("지하철 노선 목록을 조회한다.") @Test - void getLines() { - // given - ExtractableResponse createResponse1 = RestAssured - .given().log().all() + @DisplayName("지하철 노선에서 역을 삭제할 수 있다") + void test_deleteStationFromLine() { + //given + LineRequest lineRequest = new LineRequest("2호선", "초록"); + + long lineId = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) + .body(lineRequest) + .when().post("/lines") - .then().log().all(). - extract(); - ExtractableResponse createResponse2 = RestAssured - .given().log().all() + .then() + .extract() + .jsonPath() + .getLong("id"); + + StationRequest jamsilRequest = new StationRequest("잠실역"); + StationRequest bangbaeRequest = new StationRequest("방배역"); + + long jamsilStationId = RestAssured.given() .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); - } + .body(jamsilRequest) - @DisplayName("지하철 노선을 조회한다.") - @Test - void getLine() { - // given - ExtractableResponse createResponse = RestAssured - .given().log().all() + .when().post("/stations") + + .then() + .extract() + .jsonPath() + .getLong("id"); + + long bangbaeStationId = RestAssured.given() .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); + .body(bangbaeRequest) + + .when().post("/stations") + + .then() + .extract() + .jsonPath().getLong("id"); + + StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( + jamsilStationId, bangbaeStationId, 3); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(stationEnrollRequest) + + .when().post("/subway/{lineId}", lineId); + + // when, then + RestAssured.given() + .when().delete("/subway/{lineId}/stations/{stationId}", lineId, jamsilStationId) + + .then() + .statusCode(HttpStatus.NO_CONTENT.value()); } - @DisplayName("지하철 노선을 수정한다.") @Test - void updateLine() { - // given - ExtractableResponse createResponse = RestAssured - .given().log().all() + @DisplayName("지하철 노선의 역들을 조회할 수 있다.") + void test_findStationFromLine() { + //given + LineRequest lineRequest = new LineRequest("2호선", "초록"); + + long lineId = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) + .body(lineRequest) + .when().post("/lines") - .then().log().all(). - extract(); - // when - Long lineId = Long.parseLong(createResponse.header("Location").split("/")[2]); - ExtractableResponse response = RestAssured - .given().log().all() + .then() + .extract() + .jsonPath() + .getLong("id"); + + StationRequest jamsilRequest = new StationRequest("잠실역"); + StationRequest bangbaeRequest = new StationRequest("방배역"); + + long jamsilStationId = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(jamsilRequest) + + .when().post("/stations") + + .then() + .extract() + .jsonPath() + .getLong("id"); + + long bangbaeStationId = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(bangbaeRequest) + + .when().post("/stations") + + .then() + .extract() + .jsonPath().getLong("id"); + + StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( + jamsilStationId, bangbaeStationId, 3); + + RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest2) - .when().put("/lines/{lineId}", lineId) - .then().log().all() - .extract(); + .body(stationEnrollRequest) + + .when().post("/subway/{lineId}", lineId); + + // when, then + List routeMap = RestAssured.given() + .when().get("/subway/{lineId}", lineId) - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + .then() + .extract() + .body() + .jsonPath() + .getList(".", StationResponse.class); + + assertAll( + () -> assertThat(routeMap).hasSize(2), + () -> assertThat(routeMap.get(0).getId()).isEqualTo(jamsilStationId), + () -> assertThat(routeMap.get(0).getName()).isEqualTo("잠실역"), + () -> assertThat(routeMap.get(1).getId()).isEqualTo(bangbaeStationId), + () -> assertThat(routeMap.get(1).getName()).isEqualTo("방배역") + ); } - @DisplayName("지하철 노선을 제거한다.") @Test - void deleteLine() { - // given - ExtractableResponse createResponse = RestAssured - .given().log().all() + @DisplayName("모든 지하철 노선의 역들을 조회할 수 있다.") + void test_findAllStationFromLine() { + //given + LineRequest lineOneRequest = new LineRequest("1호선", "파랑"); + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(lineOneRequest) + + .when().post("/lines"); + + LineRequest lineTwoRequest = new LineRequest("2호선", "초록"); + + long lineTwoId = RestAssured.given() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) + .body(lineTwoRequest) + .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()); + + .then() + .extract() + .jsonPath() + .getLong("id"); + + StationRequest jamsilRequest = new StationRequest("잠실역"); + StationRequest bangbaeRequest = new StationRequest("방배역"); + + long jamsilStationId = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(jamsilRequest) + + .when().post("/stations") + + .then() + .extract() + .jsonPath() + .getLong("id"); + + long bangbaeStationId = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(bangbaeRequest) + + .when().post("/stations") + + .then() + .extract() + .jsonPath().getLong("id"); + + StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( + jamsilStationId, bangbaeStationId, 3); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(stationEnrollRequest) + + .when().post("/subway/{lineId}", lineTwoId); + + // when, then + RestAssured.given() + .when().get("/subway") + + .then() + .body("size()", is(2)); } } diff --git a/src/test/java/subway/integration/LinePropertyIntegrationTest.java b/src/test/java/subway/integration/LinePropertyIntegrationTest.java new file mode 100644 index 000000000..c36457671 --- /dev/null +++ b/src/test/java/subway/integration/LinePropertyIntegrationTest.java @@ -0,0 +1,190 @@ +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.presentation.dto.LineRequest; +import subway.presentation.dto.LinePropertyResponse; + +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 LinePropertyIntegrationTest 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(".", LinePropertyResponse.class).stream() + .map(LinePropertyResponse::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()); + LinePropertyResponse resultResponse = response.as(LinePropertyResponse.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/SubwayIntegrationTest.java b/src/test/java/subway/integration/SubwayIntegrationTest.java deleted file mode 100644 index dfa7b3a53..000000000 --- a/src/test/java/subway/integration/SubwayIntegrationTest.java +++ /dev/null @@ -1,268 +0,0 @@ -package subway.integration; - -import io.restassured.RestAssured; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import subway.presentation.dto.LineRequest; -import subway.presentation.dto.StationEnrollRequest; -import subway.presentation.dto.StationRequest; -import subway.presentation.dto.StationResponse; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertAll; - -@DisplayName("지하설 노선에 역 추가/삭제/조회 관련 기능") -public class SubwayIntegrationTest extends IntegrationTest { - - @Test - @DisplayName("지하철 노선에 역을 추가할 수 있다") - void test_addStationToLine() { - //given - LineRequest lineRequest = new LineRequest("2호선", "초록"); - - long lineId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest) - - .when().post("/lines") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - StationRequest jamsilRequest = new StationRequest("잠실역"); - StationRequest bangbaeRequest = new StationRequest("방배역"); - - long jamsilStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(jamsilRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - long bangbaeStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(bangbaeRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath().getLong("id"); - - StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( - jamsilStationId, bangbaeStationId, 3); - - //when, then - RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(stationEnrollRequest) - - .when().post("/subway/{lineId}", lineId) - - .then() - .statusCode(HttpStatus.CREATED.value()); - } - - @Test - @DisplayName("지하철 노선에서 역을 삭제할 수 있다") - void test_deleteStationFromLine() { - //given - LineRequest lineRequest = new LineRequest("2호선", "초록"); - - long lineId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest) - - .when().post("/lines") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - StationRequest jamsilRequest = new StationRequest("잠실역"); - StationRequest bangbaeRequest = new StationRequest("방배역"); - - long jamsilStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(jamsilRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - long bangbaeStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(bangbaeRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath().getLong("id"); - - StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( - jamsilStationId, bangbaeStationId, 3); - - RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(stationEnrollRequest) - - .when().post("/subway/{lineId}", lineId); - - // when, then - RestAssured.given() - .when().delete("/subway/{lineId}/{stationId}", lineId, jamsilStationId) - - .then() - .statusCode(HttpStatus.NO_CONTENT.value()); - } - - @Test - @DisplayName("지하철 노선의 역들을 조회할 수 있다.") - void test_findStationFromLine() { - //given - LineRequest lineRequest = new LineRequest("2호선", "초록"); - - long lineId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest) - - .when().post("/lines") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - StationRequest jamsilRequest = new StationRequest("잠실역"); - StationRequest bangbaeRequest = new StationRequest("방배역"); - - long jamsilStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(jamsilRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - long bangbaeStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(bangbaeRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath().getLong("id"); - - StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( - jamsilStationId, bangbaeStationId, 3); - - RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(stationEnrollRequest) - - .when().post("/subway/{lineId}", lineId); - - // when, then - List routeMap = RestAssured.given() - .when().get("/subway/{lineId}", lineId) - - .then() - .extract() - .body() - .jsonPath() - .getList(".", StationResponse.class); - - assertAll( - () -> assertThat(routeMap).hasSize(2), - () -> assertThat(routeMap.get(0).getId()).isEqualTo(jamsilStationId), - () -> assertThat(routeMap.get(0).getName()).isEqualTo("잠실역"), - () -> assertThat(routeMap.get(1).getId()).isEqualTo(bangbaeStationId), - () -> assertThat(routeMap.get(1).getName()).isEqualTo("방배역") - ); - } - - @Test - @DisplayName("모든 지하철 노선의 역들을 조회할 수 있다.") - void test_findAllStationFromLine() { - //given - LineRequest lineOneRequest = new LineRequest("1호선", "파랑"); - RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineOneRequest) - - .when().post("/lines"); - - LineRequest lineTwoRequest = new LineRequest("2호선", "초록"); - - long lineTwoId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineTwoRequest) - - .when().post("/lines") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - StationRequest jamsilRequest = new StationRequest("잠실역"); - StationRequest bangbaeRequest = new StationRequest("방배역"); - - long jamsilStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(jamsilRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath() - .getLong("id"); - - long bangbaeStationId = RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(bangbaeRequest) - - .when().post("/stations") - - .then() - .extract() - .jsonPath().getLong("id"); - - StationEnrollRequest stationEnrollRequest = new StationEnrollRequest( - jamsilStationId, bangbaeStationId, 3); - - RestAssured.given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(stationEnrollRequest) - - .when().post("/subway/{lineId}", lineTwoId); - - // when, then - RestAssured.given() - .when().get("/subway") - - .then() - .body("size()", is(2)); - } -} diff --git a/src/test/java/subway/presentation/LineControllerTest.java b/src/test/java/subway/presentation/LineControllerTest.java deleted file mode 100644 index 20fe83969..000000000 --- a/src/test/java/subway/presentation/LineControllerTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package subway.presentation; - -import com.fasterxml.jackson.databind.ObjectMapper; -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.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import subway.application.service.LineService; -import subway.presentation.controller.LineController; -import subway.presentation.dto.StationEnrollRequest; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - - -@WebMvcTest(LineController.class) -class LineControllerTest { - - @Autowired - private ObjectMapper objectMapper; - @Autowired - private MockMvc mockMvc; - @MockBean - private LineService lineService; - - @Test - @DisplayName("/subway/{lineId}로 POST 요청을 보낼 수 있다") - void enrollStation() throws Exception { - // given - StationEnrollRequest stationEnrollRequest = new StationEnrollRequest(1L, 2L, 1); - String body = objectMapper.writeValueAsString(stationEnrollRequest); - doNothing().when(lineService).enrollStation(any(), any()); - - // when - mockMvc.perform(post("/subway/{lineId}", 1) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - - // then - .andExpect(status().isCreated()); - } - - @Test - @DisplayName("/subway/{lineId}로 DELETE 요청을 보낼 수 있다") - void deleteStation() throws Exception { - // given - doNothing().when(lineService).deleteStation(any(), any()); - Integer lineId = 1; - - // when - mockMvc.perform(delete("/subway/{lineId}/{stationId}", lineId, 2)) - - // then - .andExpect(status().isNoContent()) - .andExpect(header().string("Location", "/line/" + lineId)); - } -} diff --git a/src/test/java/subway/presentation/controller/LineControllerTest.java b/src/test/java/subway/presentation/controller/LineControllerTest.java new file mode 100644 index 000000000..1be4de0b1 --- /dev/null +++ b/src/test/java/subway/presentation/controller/LineControllerTest.java @@ -0,0 +1,113 @@ +package subway.presentation.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import subway.application.core.service.LineService; +import subway.application.core.service.dto.out.StationResult; +import subway.presentation.controller.LineController; +import subway.presentation.dto.StationEnrollRequest; + +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +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.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest(LineController.class) +class LineControllerTest { + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private MockMvc mockMvc; + @MockBean + private LineService lineService; + + @Test + @DisplayName("/subway/{lineId}로 POST 요청을 보낼 수 있다") + void enrollStation() throws Exception { + // given + StationEnrollRequest stationEnrollRequest = new StationEnrollRequest(1L, 2L, 1); + String body = objectMapper.writeValueAsString(stationEnrollRequest); + doNothing().when(lineService).enrollStation(any()); + + // when + mockMvc.perform(post("/subway/{lineId}", 1) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + + // then + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("/subway/stations/{lineId}로 DELETE 요청을 보낼 수 있다") + void deleteStation() throws Exception { + // given + doNothing().when(lineService).deleteStation(any()); + Integer lineId = 1; + + // when + mockMvc.perform(delete("/subway/{lineId}/stations/{stationId}", lineId, 2)) + + // then + .andExpect(status().isNoContent()) + .andExpect(header().string("Location", "/line/" + lineId)); + } + + @Test + @DisplayName("/subway/{lineId}로 GET 요청을 보낼 수 있다") + void getRouteMap() throws Exception { + // given + when(lineService.findRouteMap(any())).thenReturn(List.of( + new StationResult(1L, "잠실역"), + new StationResult(2L, "방배역") + )); + Integer lineId = 1; + + // when + mockMvc.perform(get("/subway/{lineId}", lineId)) + + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[*].id").exists()) + .andExpect(jsonPath("$.[*].name").exists()); + } + + @Test + @DisplayName("/subway로 GET 요청을 보낼 수 있다") + void getAllRouteMap() throws Exception { + // given + when(lineService.findAllRouteMap()).thenReturn(Map.of( + "1호선", List.of( + new StationResult(1L, "수원역"), + new StationResult(2L, "세류역")), + "2호선", List.of( + new StationResult(3L, "잠실역"), + new StationResult(4L, "방배역") + ) + )); + + // when + mockMvc.perform(get("/subway")) + + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[*].[*].id").exists()) + .andExpect(jsonPath("$.[*].[*].name").exists()); + } +} diff --git a/src/test/java/subway/presentation/LinePropertyControllerTest.java b/src/test/java/subway/presentation/controller/LinePropertyControllerTest.java similarity index 77% rename from src/test/java/subway/presentation/LinePropertyControllerTest.java rename to src/test/java/subway/presentation/controller/LinePropertyControllerTest.java index 4fce3bb36..ea7d6380b 100644 --- a/src/test/java/subway/presentation/LinePropertyControllerTest.java +++ b/src/test/java/subway/presentation/controller/LinePropertyControllerTest.java @@ -1,4 +1,4 @@ -package subway.presentation; +package subway.presentation.controller; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; @@ -8,10 +8,9 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import subway.application.service.LinePropertyService; -import subway.presentation.controller.LinePropertyController; +import subway.application.core.service.LinePropertyService; +import subway.application.core.service.dto.out.LinePropertyResult; import subway.presentation.dto.LineRequest; -import subway.presentation.dto.LineResponse; import java.util.List; @@ -21,6 +20,7 @@ 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -39,10 +39,10 @@ public class LinePropertyControllerTest { void createLine() throws Exception { // given LineRequest lineRequest = new LineRequest("1호선", "파랑"); - LineResponse lineResponse = new LineResponse(1L, "1호선", "파랑"); + LinePropertyResult linePropertyResult = new LinePropertyResult(1L, "1호선", "파랑"); String body = objectMapper.writeValueAsString(lineRequest); - when(linePropertyService.saveLine(any())).thenReturn(lineResponse); + when(linePropertyService.saveLineProperty(any())).thenReturn(linePropertyResult); // when mockMvc.perform(post("/lines") @@ -60,12 +60,12 @@ void createLine() throws Exception { @DisplayName("/lines 로 GET 요청을 보낼 시, 모든 노선을 응답한다") void findAllLines() throws Exception { // given - List lineResponses = List.of( - new LineResponse(1L, "1호선", "파랑"), - new LineResponse(2L, "2호선", "초록") + List linePropertyResults = List.of( + new LinePropertyResult(1L, "1호선", "파랑"), + new LinePropertyResult(2L, "2호선", "초록") ); - when(linePropertyService.findLineResponses()).thenReturn(lineResponses); + when(linePropertyService.findAllLineProperty()).thenReturn(linePropertyResults); // when mockMvc.perform(get("/lines")) @@ -82,9 +82,9 @@ void findAllLines() throws Exception { @DisplayName("/lines/{id} 로 GET 요청을 보낼 시, 특정 노선을 응답한다") void findLineById() throws Exception { // given - LineResponse lineResponse = new LineResponse(1L, "1호선", "파랑"); + LinePropertyResult linePropertyResult = new LinePropertyResult(1L, "1호선", "파랑"); - when(linePropertyService.findLineResponseById(any())).thenReturn(lineResponse); + when(linePropertyService.findLinePropertyById(any())).thenReturn(linePropertyResult); // when mockMvc.perform(get("/lines/{id}", 1L)) @@ -103,10 +103,10 @@ void updateLine() throws Exception { LineRequest lineRequest = new LineRequest("1호선", "파랑"); String body = objectMapper.writeValueAsString(lineRequest); - doNothing().when(linePropertyService).updateLine(any(), any()); + doNothing().when(linePropertyService).updateLineProperty(any()); // when - mockMvc.perform(get("/lines/{id}", 1L) + mockMvc.perform(put("/lines/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) .content(body)) @@ -118,7 +118,7 @@ void updateLine() throws Exception { @DisplayName("/lines/{id} 로 DELETE 요청을 보낼 시, 특정 노선을 삭제한다") void deleteLine() throws Exception { // given - doNothing().when(linePropertyService).deleteLineById(any()); + doNothing().when(linePropertyService).deleteLinePropertyById(any()); // when mockMvc.perform(delete("/lines/{id}", 1L)) diff --git a/src/test/java/subway/presentation/StationControllerTest.java b/src/test/java/subway/presentation/controller/StationControllerTest.java similarity index 82% rename from src/test/java/subway/presentation/StationControllerTest.java rename to src/test/java/subway/presentation/controller/StationControllerTest.java index cf04da29e..456936eba 100644 --- a/src/test/java/subway/presentation/StationControllerTest.java +++ b/src/test/java/subway/presentation/controller/StationControllerTest.java @@ -1,4 +1,4 @@ -package subway.presentation; +package subway.presentation.controller; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; @@ -8,10 +8,9 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import subway.application.service.StationService; -import subway.presentation.controller.StationController; +import subway.application.core.service.StationService; +import subway.application.core.service.dto.out.StationResult; import subway.presentation.dto.StationRequest; -import subway.presentation.dto.StationResponse; import java.util.List; @@ -21,6 +20,7 @@ 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -40,10 +40,10 @@ public class StationControllerTest { void createStation() throws Exception { // given StationRequest stationRequest = new StationRequest("잠실역"); - StationResponse stationResponse = new StationResponse(1L, "잠실역"); + StationResult stationResult = new StationResult(1L, "잠실역"); String body = objectMapper.writeValueAsString(stationRequest); - when(stationService.saveStation(any())).thenReturn(stationResponse); + when(stationService.saveStation(any())).thenReturn(stationResult); // when mockMvc.perform(post("/stations") @@ -60,12 +60,12 @@ void createStation() throws Exception { @DisplayName("/stations 로 GET 요청을 보낼 시, 모든 역을 응답한다") void showStations() throws Exception { // given - List stationResponses = List.of( - new StationResponse(1L, "잠실역"), - new StationResponse(2L, "방배역") + List stationResults = List.of( + new StationResult(1L, "잠실역"), + new StationResult(2L, "방배역") ); - when(stationService.findAllStationResponses()).thenReturn(stationResponses); + when(stationService.findAllStations()).thenReturn(stationResults); // when mockMvc.perform(get("/stations")) @@ -81,8 +81,8 @@ void showStations() throws Exception { @DisplayName("/stations/{id} 로 GET 요청을 보낼 시, 특정 역을 응답한다") void showStation() throws Exception { // given - StationResponse stationResponse = new StationResponse(1L, "잠실역"); - when(stationService.findStationResponseById(any())).thenReturn(stationResponse); + StationResult stationResult = new StationResult(1L, "잠실역"); + when(stationService.findStationById(any())).thenReturn(stationResult); // when mockMvc.perform(get("/stations/{id}", 1L)) @@ -100,10 +100,10 @@ void updateLine() throws Exception { StationRequest stationRequest = new StationRequest("잠실역"); String body = objectMapper.writeValueAsString(stationRequest); - doNothing().when(stationService).updateStation(any(), any()); + doNothing().when(stationService).updateStation(any()); // when - mockMvc.perform(get("/stations/{id}", 1L) + mockMvc.perform(put("/stations/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) .content(body)) diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 000000000..3fdeef6a3 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,7 @@ +spring: + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:testdb;MODE=MySQL + driver-class-name: org.h2.Driver