diff --git a/README.md b/README.md index efb3c5540..f561fdecc 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## 도메인 - Path - - id, 상행역, 하행역, 거리를 가지는 경로 객체 + - id, 하행역, 거리를 가지는 경로 객체 - Line - id, 이름, 색, Map 컬렉션을 가짐 @@ -11,6 +11,57 @@ - Station - id, 이름을 가짐 +- ShortestWayCalculator + - Line 객체를 파라미터로 받아 최단 경로와 거리를 계산하는 객체 + +- Direction + - 노선에 경로 추가 시 상행, 하행을 결정하는 ENUM + +- AddPathStrategy + - 노선에 경로 추가시 분기와 중복 코드를 제거하기 위해 도입 + +## 기능 요구 사항 + +### 역 + +- 역을 등록할 수 있다. +- 역을 제거할 수 있다. + +### 노선과 경로 + +- 노선은 하나의 직선이다.(순환하지 않는다.) +- 노선을 조회할 수 있다. + - 노선 조회 시 노선에 해당하는 역을 상행역부터 순서대로 조회한다. +- 모든 노선을 조회할 수 있다. + - 노선 조회 시 노선에 해당하는 역을 상행역부터 순서대로 조회한다. +- 노선을 등록할 수 있다. +- 노선을 제거할 수 있다. + - 노선 제거 시에 노선의 모든 경로가 제거 된다. +- 노선에 역(경로)를 등록할 수 있다. + - 기준역, 추가할 역, 거리, 추가할 역의 방향을 고려한다. + - 역들의 사이에 추가될 경우 등록할 경로의 거리가 기존 거리보다 짧아야 한다. + - 노선에 역이 없다면 최초 등록 시 두개의 역이 경로로 등록된다. +- 노선의 역(경로)를 제거할 수 있다. + - 중간에 등록된 역이 제거된다면 제거된 역의 양 쪽의 역의 경로 거리가 합산된다. + - 노선에 역이 두개 있다면 한개를 제거할 때 두 역이 모두 제거된다. + - 역이 제거될 때 노선에 등록된 역도 제거된다. + +### 최단 경로 조회 + +- 두개 역 사이의 가장 짧은 거리를 계산한다. + - 경로와 거리, 비용도 같이 계산한다. + - 비용 정책 변경을 고려한다. +- 최단 거리에 따라 비용이 결정된다. + - 기본 정책 + - 10km 까지 1250원 + - 50km 까지 5km 당 100원의 추가 금액 + - 50km 부터 8km 당 100원의 추가 금액 + - [ ] 추가 정책 + - 노선에 따른 추가 비용이 추가된다. + - 연령별 할인 정책이 추가된다. + - 13세 ~ 18세: 운임에서 350원을 공제한 금액의 20%할인 + - 6세 ~ 12세: 운임에서 350원을 공제한 금액의 50%할인 + ## API - GET /lines/{id} @@ -94,3 +145,23 @@ - DELETE /lines/{line-id}/stations/{station-id} - 노선의 역을 삭제한다. - Response Status OK + +- GET /fee?start={id}&end={id} +- Response OK + +```json +{ + "fee": 1250, + "stations": [ + { + "id": 1, + "name": "수원" + }, + { + "id": 2, + "name": "잠실나루" + } + ] +} + +``` diff --git a/build.gradle b/build.gradle index 68d8f2558..81b1f7190 100644 --- a/build.gradle +++ b/build.gradle @@ -1,27 +1,34 @@ plugins { - id 'java' - id 'org.springframework.boot' version '2.7.9' - id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'java' + id 'org.springframework.boot' version '2.7.9' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' } sourceCompatibility = '11' repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.jgrapht:jgrapht-core:1.5.2' + implementation 'org.slf4j:slf4j-api:1.7.25' + implementation 'org.projectlombok:lombok' - implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' + testAnnotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' - testImplementation 'io.rest-assured:rest-assured:4.4.0' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' - runtimeOnly 'com.h2database:h2' + testImplementation 'io.rest-assured:rest-assured:4.4.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + runtimeOnly 'com.h2database:h2' } test { - useJUnitPlatform() -} \ No newline at end of file + useJUnitPlatform() +} diff --git a/db/subway.mv.db b/db/subway.mv.db new file mode 100644 index 000000000..4300b0d8b Binary files /dev/null and b/db/subway.mv.db differ diff --git a/db/subway.trace.db b/db/subway.trace.db new file mode 100644 index 000000000..e3f93d5ff --- /dev/null +++ b/db/subway.trace.db @@ -0,0 +1,184 @@ +2023-05-20 17:55:19 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "INSERT [*]IGNORE INTO line (name, color) VALUES (1, '1호선', '파랑'), (2, '2호선', '초록'), (3, 'empty', 'none')"; expected "INTO"; SQL statement: +INSERT IGNORE INTO line (name, color) VALUES (1, '1호선', '파랑'), (2, '2호선', '초록'), (3, 'empty', 'none') [42001-214] +2023-05-20 17:58:29 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "INSERT [*]IGNORE INTO line (name, color) VALUES (1, '1호선', '파랑'), (2, '2호선', '초록'), (3, 'empty', 'none')"; expected "INTO"; SQL statement: +INSERT IGNORE INTO line (name, color) VALUES (1, '1호선', '파랑'), (2, '2호선', '초록'), (3, 'empty', 'none') [42001-214] +2023-05-20 18:06:25 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column count does not match; SQL statement: +INSERT INTO line (name, color) VALUES (1, '1호선', '파랑'), (2, '2호선', '초록'), (3, 'empty', 'none') [21002-214] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:502) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:477) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.message.DbException.get(DbException.java:188) + at org.h2.command.dml.Insert.doPrepare(Insert.java:295) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:575) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:631) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:554) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94) + at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java) + at org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:261) + at org.springframework.jdbc.datasource.init.ResourceDatabasePopulator.populate(ResourceDatabasePopulator.java:254) + at org.springframework.jdbc.datasource.init.DatabasePopulatorUtils.execute(DatabasePopulatorUtils.java:54) + at org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer.runScripts(DataSourceScriptDatabaseInitializer.java:90) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.runScripts(AbstractScriptDatabaseInitializer.java:145) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.applyScripts(AbstractScriptDatabaseInitializer.java:107) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.applyDataScripts(AbstractScriptDatabaseInitializer.java:101) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.initializeDatabase(AbstractScriptDatabaseInitializer.java:76) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.afterPropertiesSet(AbstractScriptDatabaseInitializer.java:65) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) + at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) + at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) + at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) + at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) + at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) + at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) + at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) + at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) + at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) + at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) + at subway.SubwayApplication.main(SubwayApplication.java:10) +2023-05-20 18:06:54 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column count does not match; SQL statement: +INSERT INTO paths VALUES (1, 1, 2, 5), (2, 3, 1, 5), (2, 1, 4, 7) [21002-214] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:502) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:477) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.message.DbException.get(DbException.java:188) + at org.h2.command.dml.Insert.doPrepare(Insert.java:295) + at org.h2.command.dml.DataChangeStatement.prepare(DataChangeStatement.java:37) + at org.h2.command.Parser.prepareCommand(Parser.java:575) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:631) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:554) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94) + at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java) + at org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:261) + at org.springframework.jdbc.datasource.init.ResourceDatabasePopulator.populate(ResourceDatabasePopulator.java:254) + at org.springframework.jdbc.datasource.init.DatabasePopulatorUtils.execute(DatabasePopulatorUtils.java:54) + at org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer.runScripts(DataSourceScriptDatabaseInitializer.java:90) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.runScripts(AbstractScriptDatabaseInitializer.java:145) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.applyScripts(AbstractScriptDatabaseInitializer.java:107) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.applyDataScripts(AbstractScriptDatabaseInitializer.java:101) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.initializeDatabase(AbstractScriptDatabaseInitializer.java:76) + at org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer.afterPropertiesSet(AbstractScriptDatabaseInitializer.java:65) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) + at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) + at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) + at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) + at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) + at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) + at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) + at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887) + at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) + at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) + at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) + at subway.SubwayApplication.main(SubwayApplication.java:10) diff --git a/src/main/java/subway/application/feecalculator/DefaultFeeCalculator.java b/src/main/java/subway/application/feecalculator/DefaultFeeCalculator.java new file mode 100644 index 000000000..233659800 --- /dev/null +++ b/src/main/java/subway/application/feecalculator/DefaultFeeCalculator.java @@ -0,0 +1,34 @@ +package subway.application.feecalculator; + +import org.springframework.stereotype.Component; + +@Component +public class DefaultFeeCalculator implements FeeCalculator { + private static final int DEFAULT_FEE = 1250; + private static final int ADDITIONAL_FEE = 100; + private static final int BASIC_DISTANCE = 10; + private static final int ADDITIONAL_DISTANCE = 50; + private static final double UNIT_DISTANCE = 5d; + private static final double LONGER_UNIT_DISTANCE = 8d; + + @Override + public int calculateFee(final int distance) { + + if (distance <= BASIC_DISTANCE) { + return DEFAULT_FEE; + } + return DEFAULT_FEE + calculateAdditionalFee(distance); + } + + private int calculateAdditionalFee(final int distance) { + if (distance <= ADDITIONAL_DISTANCE) { + return calculateMathCeil(distance, BASIC_DISTANCE, UNIT_DISTANCE) * ADDITIONAL_FEE; + } + return calculateAdditionalFee(ADDITIONAL_DISTANCE) + + calculateMathCeil(distance, ADDITIONAL_DISTANCE, LONGER_UNIT_DISTANCE) * ADDITIONAL_FEE; + } + + private int calculateMathCeil(final int distance, final int subtractDistance, final double unitDistance) { + return (int) (Math.ceil((distance - subtractDistance) / unitDistance)); + } +} diff --git a/src/main/java/subway/application/feecalculator/FeeCalculator.java b/src/main/java/subway/application/feecalculator/FeeCalculator.java new file mode 100644 index 000000000..cec0bfd78 --- /dev/null +++ b/src/main/java/subway/application/feecalculator/FeeCalculator.java @@ -0,0 +1,6 @@ +package subway.application.feecalculator; + +public interface FeeCalculator { + + int calculateFee(final int distance); +} diff --git a/src/main/java/subway/application/service/FeeService.java b/src/main/java/subway/application/service/FeeService.java new file mode 100644 index 000000000..7d10b4aa0 --- /dev/null +++ b/src/main/java/subway/application/service/FeeService.java @@ -0,0 +1,39 @@ +package subway.application.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import subway.application.feecalculator.FeeCalculator; +import subway.domain.Line; +import subway.domain.ShortestPath; +import subway.domain.Station; +import subway.domain.util.ShortestPathCalculator; +import subway.dto.response.ShortestPathResponse; +import subway.dto.response.StationResponse; +import subway.persistence.repository.SubwayRepository; + +import java.util.List; + +@Service +public class FeeService { + + private final SubwayRepository subwayRepository; + private final FeeCalculator feeCalculator; + + public FeeService(final SubwayRepository subwayRepository, final FeeCalculator feeCalculator) { + this.subwayRepository = subwayRepository; + this.feeCalculator = feeCalculator; + } + + @Transactional(readOnly = true) + public ShortestPathResponse showShortestPath(final Long startStationId, final Long endStationId) { + final List lines = subwayRepository.findLines(); + final Station start = subwayRepository.findStationById(startStationId); + final Station end = subwayRepository.findStationById(endStationId); + + final ShortestPath result = ShortestPathCalculator.calculate(start, end, lines); + final int fee = feeCalculator.calculateFee(result.getDistance()); + + + return new ShortestPathResponse(fee, StationResponse.of(result.getStations())); + } +} diff --git a/src/main/java/subway/application/LineService.java b/src/main/java/subway/application/service/LineService.java similarity index 72% rename from src/main/java/subway/application/LineService.java rename to src/main/java/subway/application/service/LineService.java index 58adfb308..4c378df05 100644 --- a/src/main/java/subway/application/LineService.java +++ b/src/main/java/subway/application/service/LineService.java @@ -1,12 +1,12 @@ -package subway.application; +package subway.application.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import subway.domain.Line; import subway.domain.Station; -import subway.dto.LineRequest; -import subway.dto.LineResponse; -import subway.dto.LineWithStationResponse; +import subway.dto.request.LineRequest; +import subway.dto.response.LineResponse; +import subway.dto.response.LineWithStationResponse; import subway.persistence.dao.LineDao; import subway.persistence.repository.SubwayRepository; @@ -25,35 +25,26 @@ public LineService(final LineDao lineDao, final SubwayRepository subwayRepositor } public LineResponse saveLine(LineRequest request) { - Line persistLine = lineDao.insert(new Line(request.getName(), request.getColor())); + Line persistLine = subwayRepository.addLine(new Line(request.getName(), request.getColor())); return LineResponse.of(persistLine); } - public List findLineResponses() { - List persistLines = findLines(); - return persistLines.stream() - .map(LineResponse::of) - .collect(Collectors.toList()); - } - - public List findLines() { - return lineDao.findAll(); - } - public void updateLine(Long id, LineRequest lineUpdateRequest) { lineDao.update(new Line(id, lineUpdateRequest.getName(), lineUpdateRequest.getColor())); } public void deleteLineById(Long id) { - lineDao.deleteById(id); + subwayRepository.deleteLineById(id); } + @Transactional(readOnly = true) public LineWithStationResponse findLineById(final Long id) { final Line line = subwayRepository.findLine(id); final List stations = line.sortStations(); return LineWithStationResponse.from(line, stations); } + @Transactional(readOnly = true) public List findAllLines() { final List lines = subwayRepository.findLines(); return lines.stream() diff --git a/src/main/java/subway/application/PathService.java b/src/main/java/subway/application/service/PathService.java similarity index 69% rename from src/main/java/subway/application/PathService.java rename to src/main/java/subway/application/service/PathService.java index c070a22a8..eea59f40d 100644 --- a/src/main/java/subway/application/PathService.java +++ b/src/main/java/subway/application/service/PathService.java @@ -1,4 +1,4 @@ -package subway.application; +package subway.application.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -7,6 +7,8 @@ import subway.domain.Station; import subway.persistence.repository.SubwayRepository; +import java.util.List; + @Transactional @Service public class PathService { @@ -27,9 +29,9 @@ public void addPath( final Station targetStation = subwayRepository.findStationById(targetStationId); final Station addStation = subwayRepository.findStationById(addStationId); - line.addPath(targetStation, addStation, distance, Direction.of(direction)); + final Line lineAdded = line.addPath(targetStation, addStation, distance, Direction.of(direction)); - subwayRepository.saveLine(line); + subwayRepository.saveLine(lineAdded); } public void removeStationFromLine(final Long lineId, final Long stationId) { @@ -40,4 +42,12 @@ public void removeStationFromLine(final Long lineId, final Long stationId) { subwayRepository.saveLine(line); } + public void removeStationFromLines(final Long stationId) { + final List lines = subwayRepository.findLines(); + final Station station = subwayRepository.findStationById(stationId); + for (Line line : lines) { + line.removeStation(station); + subwayRepository.saveLine(line); + } + } } diff --git a/src/main/java/subway/application/StationService.java b/src/main/java/subway/application/service/StationService.java similarity index 64% rename from src/main/java/subway/application/StationService.java rename to src/main/java/subway/application/service/StationService.java index ee85fa859..ff677995c 100644 --- a/src/main/java/subway/application/StationService.java +++ b/src/main/java/subway/application/service/StationService.java @@ -1,11 +1,12 @@ -package subway.application; +package subway.application.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import subway.domain.Station; -import subway.dto.StationRequest; -import subway.dto.StationResponse; +import subway.dto.request.StationRequest; +import subway.dto.response.StationResponse; import subway.persistence.dao.StationDao; +import subway.persistence.repository.SubwayRepository; import java.util.List; import java.util.stream.Collectors; @@ -14,20 +15,24 @@ @Service public class StationService { private final StationDao stationDao; + private final SubwayRepository subwayRepository; - public StationService(StationDao stationDao) { + public StationService(final StationDao stationDao, final SubwayRepository subwayRepository) { this.stationDao = stationDao; + this.subwayRepository = subwayRepository; } public StationResponse saveStation(StationRequest stationRequest) { - Station station = stationDao.insert(new Station(stationRequest.getName())); - return StationResponse.of(station); + final Station persist = subwayRepository.addStation(new Station(stationRequest.getName())); + return StationResponse.of(persist); } + @Transactional(readOnly = true) public StationResponse findStationResponseById(Long id) { return StationResponse.of(stationDao.findById(id)); } + @Transactional(readOnly = true) public List findAllStationResponses() { List stations = stationDao.findAll(); diff --git a/src/main/java/subway/domain/Direction.java b/src/main/java/subway/domain/Direction.java index 5049e0d62..1c30c48a6 100644 --- a/src/main/java/subway/domain/Direction.java +++ b/src/main/java/subway/domain/Direction.java @@ -1,10 +1,20 @@ package subway.domain; +import subway.domain.addpathstrategy.AddDownPath; +import subway.domain.addpathstrategy.AddPathStrategy; +import subway.domain.addpathstrategy.AddUpPath; + import java.util.Arrays; public enum Direction { - UP, - DOWN; + UP(new AddUpPath()), + DOWN(new AddDownPath()); + + private final AddPathStrategy addPathStrategy; + + Direction(final AddPathStrategy addPathStrategy) { + this.addPathStrategy = addPathStrategy; + } public static Direction of(final String string) { return Arrays.stream(Direction.values()) @@ -12,4 +22,8 @@ public static Direction of(final String string) { .findAny() .orElseThrow(() -> new IllegalArgumentException("잘못된 요청입니다.")); } + + public AddPathStrategy getStrategy() { + return addPathStrategy; + } } diff --git a/src/main/java/subway/domain/Line.java b/src/main/java/subway/domain/Line.java index 171152a4f..8930facce 100644 --- a/src/main/java/subway/domain/Line.java +++ b/src/main/java/subway/domain/Line.java @@ -1,5 +1,7 @@ package subway.domain; +import subway.domain.addpathstrategy.AddPathStrategy; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -7,8 +9,6 @@ import java.util.Objects; import java.util.stream.Collectors; -import static subway.domain.Direction.UP; - public class Line { private final Long id; private final String name; @@ -35,17 +35,24 @@ public Line setPath(final Map paths) { } public List sortStations() { + if (paths.isEmpty()) { + return List.of(); + } final Station start = computeStartStation(); final List result = new ArrayList<>(); result.add(start); Station current = start; - while (result.size() != paths.size() + 1) { + while (sortingNotCompleted(result)) { current = paths.get(current).getNext(); result.add(current); } return result; } + private boolean sortingNotCompleted(final List result) { + return result.size() != paths.size() + 1; + } + private Station computeStartStation() { final List ups = new ArrayList<>(paths.keySet()); final List downs = paths.values().stream() @@ -55,55 +62,22 @@ private Station computeStartStation() { return ups.get(0); } - public void addPath( + public Line addPath( final Station targetStation, final Station addStation, final Integer distance, final Direction direction ) { final List stations = sortStations(); - final int index = stations.indexOf(targetStation); - if (direction == UP) { - if (paths.isEmpty()) { - paths.put(addStation, new Path(targetStation, distance)); - } - if (index == 0) { - paths.put(addStation, new Path(targetStation, distance)); - return; - } - final Station stationBefore = stations.get(index - 1); - final Path path = paths.get(stationBefore); - validatePathDistance(distance, path); - paths.put(stationBefore, new Path(addStation, path.getDistance() - distance)); - paths.put(addStation, new Path(targetStation, distance)); - } - - if (paths.isEmpty()) { - paths.put(targetStation, new Path(addStation, distance)); - } - if (index == stations.size() - 1) { - paths.put(targetStation, new Path(addStation, distance)); - return; - } - final Path path = paths.get(targetStation); - validatePathDistance(distance, path); - paths.put(targetStation, new Path(addStation, distance)); - paths.put(addStation, new Path(path.getNext(), path.getDistance() - distance)); - - } + final AddPathStrategy strategy = direction.getStrategy(); + final Map added = strategy.add(targetStation, addStation, distance, stations, paths); - private void validatePathDistance(final Integer distance, final Path path) { - if (path.isShorterThan(distance)) { - throw new IllegalArgumentException("기존 경로보다 짧은 경로를 추가해야 합니다."); - } + return setPath(added); } public void removeStation(final Station station) { final List stations = sortStations(); final int index = stations.indexOf(station); - if (index == -1) { - throw new IllegalArgumentException("삭제할 수 없는 역입니다."); - } if (paths.size() == 1) { paths.clear(); return; @@ -112,16 +86,22 @@ public void removeStation(final Station station) { paths.remove(station); return; } - if (index == stations.size() - 1) { + if (isEndOfTheLine(stations, index)) { paths.remove(stations.get(index - 1)); return; } - final Station stationBefore = stations.get(index - 1); - final Path pathBefore = paths.get(stationBefore); - final Path path = paths.get(station); - final int distance = path.sumDistance(pathBefore); - paths.remove(station); - paths.put(stationBefore, new Path(path.getNext(), distance)); + if (index > 0) { + final Station stationBefore = stations.get(index - 1); + final Path pathBefore = paths.get(stationBefore); + final Path path = paths.get(station); + final int distance = path.sumDistance(pathBefore); + paths.remove(station); + paths.put(stationBefore, new Path(path.getNext(), distance)); + } + } + + private boolean isEndOfTheLine(final List stations, final int index) { + return index == stations.size() - 1 && !stations.isEmpty(); } public Long getId() { @@ -137,7 +117,7 @@ public String getColor() { } public Map getPaths() { - return paths; + return new HashMap<>(paths); } @Override diff --git a/src/main/java/subway/domain/ShortestPath.java b/src/main/java/subway/domain/ShortestPath.java new file mode 100644 index 000000000..a9471426f --- /dev/null +++ b/src/main/java/subway/domain/ShortestPath.java @@ -0,0 +1,22 @@ +package subway.domain; + +import java.util.List; + +public class ShortestPath { + + private final int distance; + private final List stations; + + public ShortestPath(final int distance, final List stations) { + this.distance = distance; + this.stations = stations; + } + + public List getStations() { + return stations; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/subway/domain/addpathstrategy/AddDownPath.java b/src/main/java/subway/domain/addpathstrategy/AddDownPath.java new file mode 100644 index 000000000..20d8622be --- /dev/null +++ b/src/main/java/subway/domain/addpathstrategy/AddDownPath.java @@ -0,0 +1,33 @@ +package subway.domain.addpathstrategy; + +import subway.domain.Path; +import subway.domain.Station; + +import java.util.List; +import java.util.Map; + +public class AddDownPath implements AddPathStrategy { + @Override + public Map add( + final Station targetStation, + final Station addStation, + final int distance, + final List stations, + final Map paths + ) { + final int index = stations.indexOf(targetStation); + if (paths.isEmpty()) { + paths.put(targetStation, new Path(addStation, distance)); + return paths; + } + if (index == stations.size() - 1) { + paths.put(targetStation, new Path(addStation, distance)); + return paths; + } + final Path path = paths.get(targetStation); + validatePathDistance(distance, path); + paths.put(targetStation, new Path(addStation, distance)); + paths.put(addStation, new Path(path.getNext(), path.getDistance() - distance)); + return paths; + } +} diff --git a/src/main/java/subway/domain/addpathstrategy/AddPathStrategy.java b/src/main/java/subway/domain/addpathstrategy/AddPathStrategy.java new file mode 100644 index 000000000..a536a272a --- /dev/null +++ b/src/main/java/subway/domain/addpathstrategy/AddPathStrategy.java @@ -0,0 +1,24 @@ +package subway.domain.addpathstrategy; + +import subway.domain.Path; +import subway.domain.Station; + +import java.util.List; +import java.util.Map; + +public interface AddPathStrategy { + + Map add( + final Station targetStation, + final Station addStation, + final int distance, + final List stations, + final Map paths + ); + + default void validatePathDistance(final Integer distance, final Path path) { + if (path.isShorterThan(distance)) { + throw new IllegalArgumentException("기존 경로보다 짧은 경로를 추가해야 합니다."); + } + } +} diff --git a/src/main/java/subway/domain/addpathstrategy/AddUpPath.java b/src/main/java/subway/domain/addpathstrategy/AddUpPath.java new file mode 100644 index 000000000..8a53154eb --- /dev/null +++ b/src/main/java/subway/domain/addpathstrategy/AddUpPath.java @@ -0,0 +1,34 @@ +package subway.domain.addpathstrategy; + +import subway.domain.Path; +import subway.domain.Station; + +import java.util.List; +import java.util.Map; + +public class AddUpPath implements AddPathStrategy { + @Override + public Map add( + final Station targetStation, + final Station addStation, + final int distance, + final List stations, + final Map paths + ) { + final int index = stations.indexOf(targetStation); + if (paths.isEmpty()) { + paths.put(addStation, new Path(targetStation, distance)); + return paths; + } + if (index == 0) { + paths.put(addStation, new Path(targetStation, distance)); + return paths; + } + final Station stationBefore = stations.get(index - 1); + final Path path = paths.get(stationBefore); + validatePathDistance(distance, path); + paths.put(stationBefore, new Path(addStation, path.getDistance() - distance)); + paths.put(addStation, new Path(targetStation, distance)); + return paths; + } +} diff --git a/src/main/java/subway/domain/util/ShortestPathCalculator.java b/src/main/java/subway/domain/util/ShortestPathCalculator.java new file mode 100644 index 000000000..11c874a7d --- /dev/null +++ b/src/main/java/subway/domain/util/ShortestPathCalculator.java @@ -0,0 +1,46 @@ +package subway.domain.util; + +import subway.domain.Line; +import subway.domain.Path; +import subway.domain.ShortestPath; +import subway.domain.Station; +import subway.support.Dijkstra; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ShortestPathCalculator { + + private static final Dijkstra DIJKSTRA = new Dijkstra<>(); + + public static ShortestPath calculate(final Station start, final Station end, final List lines) { + final List starts = new ArrayList<>(); + final List ends = new ArrayList<>(); + final List distances = new ArrayList<>(); + parseLines(lines, starts, ends, distances); + + final var result = DIJKSTRA.shortestPath(starts, ends, distances, start, end); + + try { + return new ShortestPath(((int) result.getWeight()), result.getVertexList()); + } catch (RuntimeException e) { + throw new IllegalArgumentException("갈 수 없는 경로입니다."); + } + } + + private static void parseLines(final List lines, final List starts, final List ends, final List distances) { + for (final Line line : lines) { + final Map paths = line.getPaths(); + parseLine(starts, ends, distances, paths); + } + } + + private static void parseLine(final List starts, final List ends, final List distances, final Map paths) { + for (final Map.Entry entry : paths.entrySet()) { + starts.add(entry.getKey()); + ends.add(entry.getValue().getNext()); + distances.add(entry.getValue().getDistance()); + } + } +} diff --git a/src/main/java/subway/dto/AddPathRequest.java b/src/main/java/subway/dto/request/AddPathRequest.java similarity index 60% rename from src/main/java/subway/dto/AddPathRequest.java rename to src/main/java/subway/dto/request/AddPathRequest.java index a80437f78..eb935c256 100644 --- a/src/main/java/subway/dto/AddPathRequest.java +++ b/src/main/java/subway/dto/request/AddPathRequest.java @@ -1,11 +1,24 @@ -package subway.dto; +package subway.dto.request; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; public class AddPathRequest { - private final Long targetStationId; - private final Long addStationId; - private final Integer distance; - private final String direction; + @NotNull + @Positive + private Long targetStationId; + @NotNull + @Positive + private Long addStationId; + @Positive + private Integer distance; + @NotBlank + private String direction; + + public AddPathRequest() { + } public AddPathRequest(final Long targetStationId, final Long addStationId, final Integer distance, final String direction) { this.targetStationId = targetStationId; diff --git a/src/main/java/subway/dto/LineRequest.java b/src/main/java/subway/dto/request/LineRequest.java similarity index 63% rename from src/main/java/subway/dto/LineRequest.java rename to src/main/java/subway/dto/request/LineRequest.java index 16cb5bf76..19e98dc24 100644 --- a/src/main/java/subway/dto/LineRequest.java +++ b/src/main/java/subway/dto/request/LineRequest.java @@ -1,13 +1,17 @@ -package subway.dto; +package subway.dto.request; + +import javax.validation.constraints.NotBlank; public class LineRequest { + @NotBlank private String name; + @NotBlank private String color; public LineRequest() { } - public LineRequest(String name, String color) { + public LineRequest(final String name, final String color) { this.name = name; this.color = color; } diff --git a/src/main/java/subway/dto/StationRequest.java b/src/main/java/subway/dto/request/StationRequest.java similarity index 57% rename from src/main/java/subway/dto/StationRequest.java rename to src/main/java/subway/dto/request/StationRequest.java index 15175303d..cbb352e45 100644 --- a/src/main/java/subway/dto/StationRequest.java +++ b/src/main/java/subway/dto/request/StationRequest.java @@ -1,12 +1,15 @@ -package subway.dto; +package subway.dto.request; + +import javax.validation.constraints.NotBlank; public class StationRequest { + @NotBlank private String name; public StationRequest() { } - public StationRequest(String name) { + public StationRequest(final String name) { this.name = name; } diff --git a/src/main/java/subway/dto/LineResponse.java b/src/main/java/subway/dto/response/LineResponse.java similarity index 71% rename from src/main/java/subway/dto/LineResponse.java rename to src/main/java/subway/dto/response/LineResponse.java index 4b1b4edd6..2d85c9383 100644 --- a/src/main/java/subway/dto/LineResponse.java +++ b/src/main/java/subway/dto/response/LineResponse.java @@ -1,11 +1,14 @@ -package subway.dto; +package subway.dto.response; import subway.domain.Line; public class LineResponse { - private final Long id; - private final String name; - private final String color; + private Long id; + private String name; + private String color; + + public LineResponse() { + } public LineResponse(final Long id, final String name, final String color) { this.id = id; @@ -13,7 +16,7 @@ public LineResponse(final Long id, final String name, final String color) { this.color = color; } - public static LineResponse of(Line line) { + public static LineResponse of(final Line line) { return new LineResponse(line.getId(), line.getName(), line.getColor()); } diff --git a/src/main/java/subway/dto/LineWithStationResponse.java b/src/main/java/subway/dto/response/LineWithStationResponse.java similarity index 72% rename from src/main/java/subway/dto/LineWithStationResponse.java rename to src/main/java/subway/dto/response/LineWithStationResponse.java index 4c2ca977d..50f91f8bd 100644 --- a/src/main/java/subway/dto/LineWithStationResponse.java +++ b/src/main/java/subway/dto/response/LineWithStationResponse.java @@ -1,4 +1,4 @@ -package subway.dto; +package subway.dto.response; import subway.domain.Line; import subway.domain.Station; @@ -7,10 +7,13 @@ public class LineWithStationResponse { - private final Long id; - private final String name; - private final String color; - private final List stations; + private Long id; + private String name; + private String color; + private List stations; + + public LineWithStationResponse() { + } public LineWithStationResponse( final Long id, final String name, @@ -24,6 +27,9 @@ public LineWithStationResponse( } public static LineWithStationResponse from(final Line line, final List stations) { + if (stations.isEmpty()) { + return new LineWithStationResponse(line.getId(), line.getName(), line.getColor(), List.of()); + } return new LineWithStationResponse(line.getId(), line.getName(), line.getColor(), StationResponse.of(stations)); } diff --git a/src/main/java/subway/dto/response/ShortestPathResponse.java b/src/main/java/subway/dto/response/ShortestPathResponse.java new file mode 100644 index 000000000..dee8003c5 --- /dev/null +++ b/src/main/java/subway/dto/response/ShortestPathResponse.java @@ -0,0 +1,25 @@ +package subway.dto.response; + +import java.util.List; + +public class ShortestPathResponse { + + private int fee; + private List stations; + + public ShortestPathResponse() { + } + + public ShortestPathResponse(final int fee, final List stations) { + this.fee = fee; + this.stations = stations; + } + + public int getFee() { + return fee; + } + + public List getStations() { + return stations; + } +} diff --git a/src/main/java/subway/dto/StationResponse.java b/src/main/java/subway/dto/response/StationResponse.java similarity index 67% rename from src/main/java/subway/dto/StationResponse.java rename to src/main/java/subway/dto/response/StationResponse.java index c338918dd..6336140cc 100644 --- a/src/main/java/subway/dto/StationResponse.java +++ b/src/main/java/subway/dto/response/StationResponse.java @@ -1,4 +1,4 @@ -package subway.dto; +package subway.dto.response; import subway.domain.Station; @@ -9,16 +9,19 @@ public class StationResponse { private Long id; private String name; - public StationResponse(Long id, String name) { + public StationResponse() { + } + + public StationResponse(final Long id, final String name) { this.id = id; this.name = name; } - public static StationResponse of(Station station) { + public static StationResponse of(final Station station) { return new StationResponse(station.getId(), station.getName()); } - public static List of(List stations) { + public static List of(final List stations) { return stations.stream() .map(StationResponse::of) .collect(Collectors.toList()); diff --git a/src/main/java/subway/exception/DuplicatedNameException.java b/src/main/java/subway/exception/DuplicatedNameException.java new file mode 100644 index 000000000..bd37ab29b --- /dev/null +++ b/src/main/java/subway/exception/DuplicatedNameException.java @@ -0,0 +1,7 @@ +package subway.exception; + +public class DuplicatedNameException extends RuntimeException { + public DuplicatedNameException() { + super("중복된 이름으로 만들 수 없습니다."); + } +} diff --git a/src/main/java/subway/exception/GlobalExceptionHandler.java b/src/main/java/subway/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..d024d030b --- /dev/null +++ b/src/main/java/subway/exception/GlobalExceptionHandler.java @@ -0,0 +1,43 @@ +package subway.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler + public ResponseEntity handleIllegalArgumentException(final IllegalArgumentException exception) { + log.error(exception.getMessage()); + return ResponseEntity.badRequest().body(exception.getMessage()); + } + + @ExceptionHandler({StationNotFoundException.class, LineNotFoundException.class}) + public ResponseEntity handleLineNotFoundException(RuntimeException exception) { + log.error("{}: 잘못된 삭제 요청", exception.getClass()); + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler + public ResponseEntity handleDuplicatedNameException(final DuplicatedNameException exception) { + log.error(exception.getMessage()); + return ResponseEntity.badRequest().body(exception.getMessage()); + } + + @ExceptionHandler + public ResponseEntity handleNotFound(final EmptyResultDataAccessException exception) { + log.error("{}: 경로를 찾을 수 없습니다", exception.getClass()); + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler + public ResponseEntity handleException(final Exception exception) { + log.error(exception.getMessage()); + return ResponseEntity.internalServerError().build(); + } +} diff --git a/src/main/java/subway/exception/LineNotFoundException.java b/src/main/java/subway/exception/LineNotFoundException.java new file mode 100644 index 000000000..0d40ef7da --- /dev/null +++ b/src/main/java/subway/exception/LineNotFoundException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class LineNotFoundException extends RuntimeException { + + public LineNotFoundException() { + super("존재하지 않는 노선입니다."); + } +} diff --git a/src/main/java/subway/exception/StationNotFoundException.java b/src/main/java/subway/exception/StationNotFoundException.java new file mode 100644 index 000000000..352268d5a --- /dev/null +++ b/src/main/java/subway/exception/StationNotFoundException.java @@ -0,0 +1,8 @@ +package subway.exception; + +public class StationNotFoundException extends RuntimeException { + + public StationNotFoundException() { + super("존재하지 않는 역입니다."); + } +} diff --git a/src/main/java/subway/persistence/dao/LineDao.java b/src/main/java/subway/persistence/dao/LineDao.java index a4e5af50f..89bf2d416 100644 --- a/src/main/java/subway/persistence/dao/LineDao.java +++ b/src/main/java/subway/persistence/dao/LineDao.java @@ -5,8 +5,8 @@ import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import subway.domain.Line; +import subway.exception.LineNotFoundException; -import javax.sql.DataSource; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,9 +23,9 @@ public class LineDao { rs.getString("color") ); - public LineDao(JdbcTemplate jdbcTemplate, DataSource dataSource) { + public LineDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; - this.insertAction = new SimpleJdbcInsert(dataSource) + this.insertAction = new SimpleJdbcInsert(jdbcTemplate) .withTableName("line") .usingGeneratedKeyColumns("id"); } @@ -56,6 +56,9 @@ public void update(Line newLine) { } public void deleteById(Long id) { - jdbcTemplate.update("delete from Line where id = ?", id); + final int affected = jdbcTemplate.update("delete from Line where id = ?", id); + if (affected == 0) { + throw new LineNotFoundException(); + } } } diff --git a/src/main/java/subway/persistence/dao/PathDao.java b/src/main/java/subway/persistence/dao/PathDao.java index c0af8c7bc..a04a15fef 100644 --- a/src/main/java/subway/persistence/dao/PathDao.java +++ b/src/main/java/subway/persistence/dao/PathDao.java @@ -31,14 +31,10 @@ public List findAll() { return jdbcTemplate.query(sql, mapper); } - public void clear(final Long lineId) { - final String sql = "DELETE TABLE paths WHERE line_id = ?"; - jdbcTemplate.update(sql, lineId); - } - public void addAll(final Long lineId, final Map paths, List stations) { final String sql = "INSERT INTO paths (line_id, up_station_id, down_station_id, distance) " + "VALUES (? ,? ,? ,?)"; + stations.remove(stations.size() - 1); jdbcTemplate.batchUpdate(sql, stations, 100, (ps, station) -> { ps.setLong(1, lineId); ps.setLong(2, station.getId()); @@ -46,4 +42,9 @@ public void addAll(final Long lineId, final Map paths, List findLines() { return lines.stream() .map(line -> line.setPath( - entitiesToPath(pathEntitiesByLineId.get(line.getId()), mapper)) + entitiesToPath(pathEntitiesByLineId.getOrDefault(line.getId(), List.of()), mapper)) ) .collect(Collectors.toList()); } @@ -72,7 +74,32 @@ public Station findStationById(final Long id) { } public void saveLine(final Line line) { - pathDao.clear(line.getId()); - pathDao.addAll(line.getId(), line.getPaths(), line.sortStations()); + pathDao.deleteByLineId(line.getId()); + final List stations = line.sortStations(); + if (stations.isEmpty()) { + return; + } + pathDao.addAll(line.getId(), line.getPaths(), stations); + } + + public void deleteLineById(final Long id) { + lineDao.deleteById(id); + pathDao.deleteByLineId(id); + } + + public Line addLine(final Line line) { + try { + return lineDao.insert(line); + } catch (DataIntegrityViolationException e) { + throw new DuplicatedNameException(); + } + } + + public Station addStation(final Station station) { + try { + return stationDao.insert(station); + } catch (DataIntegrityViolationException e) { + throw new DuplicatedNameException(); + } } } diff --git a/src/main/java/subway/support/Dijkstra.java b/src/main/java/subway/support/Dijkstra.java new file mode 100644 index 000000000..4dec97748 --- /dev/null +++ b/src/main/java/subway/support/Dijkstra.java @@ -0,0 +1,36 @@ +package subway.support; + +import org.jgrapht.GraphPath; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; + +import java.util.List; + +public class Dijkstra { + + private final WeightedMultigraph graph = + new WeightedMultigraph<>(DefaultWeightedEdge.class); + + public final GraphPath shortestPath( + final List startValues, + final List nextValues, + final List distances, + final T start, + final T end + ) { + for (int i = 0; i < startValues.size(); i++) { + graph.addVertex(startValues.get(i)); + graph.addVertex(nextValues.get(i)); + } + addEdges(startValues, nextValues, distances); + + return new DijkstraShortestPath<>(graph).getPath(start, end); + } + + private void addEdges(final List values, final List nextValues, final List distances) { + for (int i = 0; i < values.size(); i++) { + graph.setEdgeWeight(graph.addEdge(values.get(i), nextValues.get(i)), distances.get(i)); + } + } +} diff --git a/src/main/java/subway/ui/FeeController.java b/src/main/java/subway/ui/FeeController.java new file mode 100644 index 000000000..8e24d095e --- /dev/null +++ b/src/main/java/subway/ui/FeeController.java @@ -0,0 +1,30 @@ +package subway.ui; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import subway.application.service.FeeService; +import subway.dto.response.ShortestPathResponse; + +@RequestMapping("/fee") +@RestController +public class FeeController { + + private final FeeService feeService; + + public FeeController(final FeeService feeService) { + this.feeService = feeService; + } + + @GetMapping + public ResponseEntity findShortestWay( + @RequestParam("start") Long startStationId, + @RequestParam("end") Long endStationId + ) { + final var shortestWayResponse = feeService.showShortestPath(startStationId, endStationId); + + return ResponseEntity.ok(shortestWayResponse); + } +} diff --git a/src/main/java/subway/ui/LineController.java b/src/main/java/subway/ui/LineController.java index 2de5e4dab..4ef03f57f 100644 --- a/src/main/java/subway/ui/LineController.java +++ b/src/main/java/subway/ui/LineController.java @@ -2,7 +2,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -10,13 +9,13 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import subway.application.LineService; -import subway.dto.LineRequest; -import subway.dto.LineResponse; -import subway.dto.LineWithStationResponse; +import subway.application.service.LineService; +import subway.dto.request.LineRequest; +import subway.dto.response.LineResponse; +import subway.dto.response.LineWithStationResponse; +import javax.validation.Valid; import java.net.URI; -import java.sql.SQLException; import java.util.List; @RestController @@ -30,7 +29,7 @@ public LineController(LineService lineService) { } @PostMapping - public ResponseEntity createLine(@RequestBody LineRequest lineRequest) { + public ResponseEntity createLine(@Valid @RequestBody LineRequest lineRequest) { LineResponse line = lineService.saveLine(lineRequest); return ResponseEntity.created(URI.create("/lines/" + line.getId())).body(line); } @@ -46,7 +45,7 @@ public ResponseEntity findLineById(@PathVariable Long i } @PutMapping("/{id}") - public ResponseEntity updateLine(@PathVariable Long id, @RequestBody LineRequest lineUpdateRequest) { + public ResponseEntity updateLine(@PathVariable Long id, @Valid @RequestBody LineRequest lineUpdateRequest) { lineService.updateLine(id, lineUpdateRequest); return ResponseEntity.ok().build(); } @@ -56,9 +55,4 @@ public ResponseEntity deleteLine(@PathVariable Long id) { lineService.deleteLineById(id); return ResponseEntity.noContent().build(); } - - @ExceptionHandler(SQLException.class) - public ResponseEntity handleSQLException() { - return ResponseEntity.badRequest().build(); - } } diff --git a/src/main/java/subway/ui/PathController.java b/src/main/java/subway/ui/PathController.java index b90ad1e63..c73f56a47 100644 --- a/src/main/java/subway/ui/PathController.java +++ b/src/main/java/subway/ui/PathController.java @@ -7,8 +7,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import subway.application.PathService; -import subway.dto.AddPathRequest; +import subway.application.service.PathService; +import subway.dto.request.AddPathRequest; + +import javax.validation.Valid; @RequestMapping("/lines/{line-id}/stations") @RestController @@ -23,7 +25,7 @@ public PathController(final PathService pathService) { @PostMapping public ResponseEntity addStationInPath( @PathVariable("line-id") final Long lineId, - @RequestBody final AddPathRequest addPathRequest + @Valid @RequestBody final AddPathRequest addPathRequest ) { pathService.addPath( lineId, diff --git a/src/main/java/subway/ui/StationController.java b/src/main/java/subway/ui/StationController.java index 5bf52a9a9..bf9b6063b 100644 --- a/src/main/java/subway/ui/StationController.java +++ b/src/main/java/subway/ui/StationController.java @@ -1,26 +1,36 @@ package subway.ui; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import subway.dto.StationRequest; -import subway.dto.StationResponse; -import subway.application.StationService; - +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import subway.application.service.PathService; +import subway.application.service.StationService; +import subway.dto.request.StationRequest; +import subway.dto.response.StationResponse; + +import javax.validation.Valid; import java.net.URI; -import java.sql.SQLException; import java.util.List; @RestController @RequestMapping("/stations") public class StationController { private final StationService stationService; + private final PathService pathService; - public StationController(StationService stationService) { + public StationController(final StationService stationService, final PathService pathService) { this.stationService = stationService; + this.pathService = pathService; } @PostMapping - public ResponseEntity createStation(@RequestBody StationRequest stationRequest) { + public ResponseEntity createStation(@Valid @RequestBody StationRequest stationRequest) { StationResponse station = stationService.saveStation(stationRequest); return ResponseEntity.created(URI.create("/stations/" + station.getId())).body(station); } @@ -36,19 +46,15 @@ public ResponseEntity showStation(@PathVariable Long id) { } @PutMapping("/{id}") - public ResponseEntity updateStation(@PathVariable Long id, @RequestBody StationRequest stationRequest) { + public ResponseEntity updateStation(@PathVariable Long id, @Valid @RequestBody StationRequest stationRequest) { stationService.updateStation(id, stationRequest); return ResponseEntity.ok().build(); } @DeleteMapping("/{id}") public ResponseEntity deleteStation(@PathVariable Long id) { + pathService.removeStationFromLines(id); stationService.deleteStationById(id); return ResponseEntity.noContent().build(); } - - @ExceptionHandler(SQLException.class) - public ResponseEntity handleSQLException() { - return ResponseEntity.badRequest().build(); - } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d4bdbcb40..e510cc83f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,5 @@ -spring.datasource.url=jdbc:h2:mem:testdb;MODE=mysql +spring.datasource.url=jdbc:h2:file:./db/subway +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.h2.console.enabled=true +spring.sql.init.mode=never diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 31465c101..f61420b64 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,16 +1,15 @@ -INSERT INTO line (name, color) -VALUES ('1호선', '파랑'), - ('2호선', '초록'); - -INSERT INTO station (name) -VALUES ('수원'), - ('잠실나루'), - ('의왕'), - ('선릉'); - -INSERT INTO paths (line_id, up_station_id, down_station_id, distance) -VALUES (1, 1, 2, 5), - (2, 3, 1, 5), - (2, 1, 4, 7); - - +INSERT INTO line +VALUES (1, '1호선', '파랑'), + (2, '2호선', '초록'), + (3, 'empty', 'none'); + +INSERT INTO station +VALUES (1, '수원'), + (2, '잠실나루'), + (3, '의왕'), + (4, '선릉'); + +INSERT INTO paths +VALUES (1, 1, 1, 2, 5), + (2, 2, 3, 1, 5), + (3, 2, 1, 4, 7); diff --git a/src/main/resources/logback-access.xml b/src/main/resources/logback-access.xml index 38e0823f4..b53becb81 100644 --- a/src/main/resources/logback-access.xml +++ b/src/main/resources/logback-access.xml @@ -1,8 +1,9 @@ - %n###### HTTP Request ######%n%fullRequest%n###### HTTP Response ######%n%fullResponse%n%n + %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 c81fdd4de..9821a8cca 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,29 +1,24 @@ -DROP TABLE station IF EXISTS; -DROP TABLE line IF EXISTS; -DROP TABLE paths IF EXISTS; - - -create table if not exists STATION +CREATE TABLE IF NOT EXISTS station ( - id bigint auto_increment not null, - name varchar(255) not null unique, - primary key(id) + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL UNIQUE, + PRIMARY KEY (id) ); -create table if not exists LINE +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) + 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 paths ( - id BIGINT AUTO_INCREMENT NOT NULL, - line_id BIGINT NOT NULL, - up_station_id BIGINT NOT NULL, - down_station_id BIGINT NOT NULL, - distance INT NOT NULL, - PRIMARY KEY(id) + id BIGINT AUTO_INCREMENT NOT NULL, + line_id BIGINT NOT NULL, + up_station_id BIGINT NOT NULL, + down_station_id BIGINT NOT NULL, + distance INT NOT NULL, + PRIMARY KEY (id) ); diff --git a/src/test/java/study/dijkTest.java b/src/test/java/study/dijkTest.java new file mode 100644 index 000000000..2e7b7db61 --- /dev/null +++ b/src/test/java/study/dijkTest.java @@ -0,0 +1,37 @@ +package study; + +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class dijkTest { + + @Test + public void getDijkstraShortestPath() { + WeightedMultigraph graph + = new WeightedMultigraph(DefaultWeightedEdge.class); + graph.addVertex("v1"); + graph.addVertex("v2"); + graph.addVertex("v3"); + graph.addVertex("v3"); + graph.setEdgeWeight(graph.addEdge("v1", "v2"), 2); + graph.setEdgeWeight(graph.addEdge("v2", "v3"), 2); + graph.setEdgeWeight(graph.addEdge("v1", "v3"), 100); + + DijkstraShortestPath dijkstraShortestPath + = new DijkstraShortestPath(graph); + List shortestPath + = dijkstraShortestPath.getPath("v3", "v1").getVertexList(); + + assertThat(shortestPath.size()).isEqualTo(3); + final double weight = dijkstraShortestPath.getPath("v3", "v1").getWeight(); + + System.out.println(weight); + } + +} diff --git a/src/test/java/subway/application/service/FeeServiceTest.java b/src/test/java/subway/application/service/FeeServiceTest.java new file mode 100644 index 000000000..a2019ed79 --- /dev/null +++ b/src/test/java/subway/application/service/FeeServiceTest.java @@ -0,0 +1,20 @@ +package subway.application.service; + +import org.junit.jupiter.api.Test; +import subway.dto.response.ShortestPathResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class FeeServiceTest extends ServiceTest { + + @Test + void 가장_짧은_경로와_요금을_계산한다() { + final ShortestPathResponse shortestPathResponse = feeService.showShortestPath(1L, 2L); + + assertAll( + () -> assertThat(shortestPathResponse.getFee()).isEqualTo(1250), + () -> assertThat(shortestPathResponse.getStations()).hasSize(2) + ); + } +} diff --git a/src/test/java/subway/application/service/PathServiceTest.java b/src/test/java/subway/application/service/PathServiceTest.java new file mode 100644 index 000000000..c4a7ffca2 --- /dev/null +++ b/src/test/java/subway/application/service/PathServiceTest.java @@ -0,0 +1,15 @@ +package subway.application.service; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; + + +class PathServiceTest extends ServiceTest { + + @Test + void 모든_노선에서_역을_삭제한다() { + assertThatCode(() -> pathService.removeStationFromLines(1L)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/subway/application/service/ServiceTest.java b/src/test/java/subway/application/service/ServiceTest.java new file mode 100644 index 000000000..c158b4ec2 --- /dev/null +++ b/src/test/java/subway/application/service/ServiceTest.java @@ -0,0 +1,24 @@ +package subway.application.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; + + +@Sql({"/scheme.sql", "/data.sql"}) +@Sql(value = "/truncate.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +@SpringBootTest +public abstract class ServiceTest { + + @Autowired + protected PathService pathService; + + @Autowired + protected LineService lineService; + + @Autowired + protected FeeService feeService; + + @Autowired + protected StationService stationService; +} diff --git a/src/test/java/subway/application/service/feecalculator/DefaultDistanceFeePolicyTest.java b/src/test/java/subway/application/service/feecalculator/DefaultDistanceFeePolicyTest.java new file mode 100644 index 000000000..e2738e039 --- /dev/null +++ b/src/test/java/subway/application/service/feecalculator/DefaultDistanceFeePolicyTest.java @@ -0,0 +1,23 @@ +package subway.application.service.feecalculator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import subway.application.feecalculator.DefaultFeeCalculator; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultFeeCalculatorTest { + + @DisplayName("요금을 계산한다.") + @ParameterizedTest + @CsvSource(value = {"9, 1250", "10, 1250", "14, 1350", "15, 1350", "50, 2050", "51, 2150", "58, 2150", "59, 2250"}) + void calculateFee(int distance, int fee) { + final DefaultFeeCalculator calculator = new DefaultFeeCalculator(); + + final int result = calculator.calculateFee(distance); + + assertThat(result).isEqualTo(fee); + } + +} diff --git a/src/test/java/subway/domain/DirectionTest.java b/src/test/java/subway/domain/DirectionTest.java new file mode 100644 index 000000000..25c611972 --- /dev/null +++ b/src/test/java/subway/domain/DirectionTest.java @@ -0,0 +1,32 @@ +package subway.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static subway.domain.Direction.UP; +import static subway.domain.Direction.of; + +class DirectionTest { + + @DisplayName("대소문자에 상관없이 enum 객체를 생성한다.") + @Test + void createIgnoringCaseTest() { + final String up = "up"; + + final Direction direction = of(up); + + assertThat(direction).isSameAs(UP); + } + + @DisplayName("잘못된 인자가 들어오면 예외를 던진다.") + @Test + void createExceptionTest() { + final String mal = "exception"; + + assertThatThrownBy(() -> of(mal)) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/subway/domain/LineTest.java b/src/test/java/subway/domain/LineTest.java index ce6d14f8e..628d02e4d 100644 --- a/src/test/java/subway/domain/LineTest.java +++ b/src/test/java/subway/domain/LineTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -9,6 +10,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static subway.domain.Direction.DOWN; import static subway.domain.Direction.UP; @@ -29,64 +31,91 @@ void sortTest() { assertThat(stations).map(Station::getName).containsExactly("성남", "성대", "강남"); } - @DisplayName("역을 추가한다.") + @DisplayName("경로가 없을 때 정렬하면 빈 리스트를 반환한다.") @Test - void addTest() { - //given - final Station stationA = new Station(1L, "A"); - final Station stationB = new Station(3L, "B"); - final Station stationC = new Station(2L, "C"); - final Line line = new Line(1L, "1호선", "파랑", - new HashMap<>(Map.of( - stationA, new Path(stationB, 5) - , stationB, new Path(stationC, 10) - )) - ); - final Station newStation = new Station(4L, "D"); - - //when - line.addPath(stationA, newStation, 3, DOWN); - final Path pathAfterStationA = line.getPaths().get(stationA); - final Path pathAfterNewStation = line.getPaths().get(newStation); - - //then - assertAll( - () -> assertThat(pathAfterStationA.getNext()).isEqualTo(newStation), - () -> assertThat(pathAfterStationA.getDistance()).isEqualTo(3), - () -> assertThat(pathAfterNewStation.getDistance()).isEqualTo(2) - ); + void sortEmptyTest() { + final Line line = new Line(1L, "1호선", "blue", Map.of()); + + final List stations = line.sortStations(); + + assertThat(stations).isEmpty(); } - @DisplayName("역을 추가한다.") - @Test - void addTest2() { - //given + @Nested + class 역을_추가한다 { + final Station stationA = new Station(1L, "A"); final Station stationB = new Station(3L, "B"); final Station stationC = new Station(2L, "C"); + final Station stationD = new Station(4L, "D"); final Line line = new Line(1L, "1호선", "파랑", new HashMap<>(Map.of( stationA, new Path(stationB, 5) , stationB, new Path(stationC, 10) )) ); - final Station newStation = new Station(4L, "D"); - - //when - line.addPath(stationA, newStation, 3, UP); - final Station stationAfterNew = line.getPaths().get(newStation).getNext(); - //then - assertThat(stationAfterNew).isEqualTo(stationA); + @Test + void A_뒤에_역을_추가한다() { + + //when + line.addPath(stationA, stationD, 3, DOWN); + final Path pathAfterStationA = line.getPaths().get(stationA); + final Path pathAfterNewStation = line.getPaths().get(stationD); + + //then + assertAll( + () -> assertThat(pathAfterStationA.getNext()).isEqualTo(stationD), + () -> assertThat(pathAfterStationA.getDistance()).isEqualTo(3), + () -> assertThat(pathAfterNewStation.getDistance()).isEqualTo(2) + ); + } + + @Test + void 맨_앞에_역을_추가한다() { + + //when + line.addPath(stationA, stationD, 3, UP); + final Path path = line.getPaths().get(stationD); + + //then + assertAll( + () -> assertThat(path.getNext()).isEqualTo(stationA), + () -> assertThat(path.getDistance()).isEqualTo(3), + () -> assertThat(line.getPaths().get(stationA).getNext()).isEqualTo(stationB) + ); + } + + @Test + void 맨_뒤에_역을_추가한다() { + + //when + line.addPath(stationC, stationD, 4, DOWN); + final Path path = line.getPaths().get(stationC); + + //then + assertAll( + () -> assertThat(path.getNext()).isEqualTo(stationD), + () -> assertThat(path.getDistance()).isEqualTo(4) + ); + } + + @Test + void 역을_추가할_때_거리를_초과하면_예외를_던진다() { + + //when,then + assertThatThrownBy(() -> line.addPath(stationA, stationD, 6, DOWN)) + .isInstanceOf(IllegalArgumentException.class); + } } - @DisplayName("역을 삭제한다.") - @Test - void removeStationTest() { - //given + @Nested + class 역을_삭제한다 { + final Station stationA = new Station(1L, "A"); final Station stationB = new Station(3L, "B"); final Station stationC = new Station(2L, "C"); + final Line line = new Line(1L, "1호선", "파랑", new HashMap<>(Map.of( stationA, new Path(stationB, 5) @@ -94,13 +123,40 @@ stationA, new Path(stationB, 5) )) ); - //when - line.removeStation(stationB); - - //then - Assertions.assertAll( - () -> assertThat(line.getPaths().get(stationA).getDistance()).isEqualTo(15), - () -> assertThat(line.getPaths().get(stationA).getNext()).isEqualTo(stationC) - ); + @Test + void 중간_역을_삭제한다() { + //when + line.removeStation(stationB); + + //then + Assertions.assertAll( + () -> assertThat(line.getPaths().get(stationA).getDistance()).isEqualTo(15), + () -> assertThat(line.getPaths().get(stationA).getNext()).isEqualTo(stationC) + ); + } + + @Test + void 마지막_역을_삭제한다() { + //when + line.removeStation(stationC); + + //then + Assertions.assertAll( + () -> assertThat(line.getPaths()).hasSize(1), + () -> assertThat(line.getPaths().get(stationB)).isNull() + ); + } + + @Test + void 처음_역을_삭제한다() { + //when + line.removeStation(stationA); + + //then + Assertions.assertAll( + () -> assertThat(line.getPaths()).hasSize(1), + () -> assertThat(line.getPaths().get(stationA)).isNull() + ); + } } } diff --git a/src/test/java/subway/domain/ShortestPathCalculatorTest.java b/src/test/java/subway/domain/ShortestPathCalculatorTest.java new file mode 100644 index 000000000..99378ccd3 --- /dev/null +++ b/src/test/java/subway/domain/ShortestPathCalculatorTest.java @@ -0,0 +1,112 @@ +package subway.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import subway.domain.util.ShortestPathCalculator; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.List.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ShortestPathCalculatorTest { + + @DisplayName("최단 거리를 계산한다.") + @Test + void computeShortestPath() { + //given + final Station stationA = new Station(1L, "A"); + final Station stationB = new Station(3L, "B"); + final Station stationC = new Station(2L, "C"); + final Line line = new Line(1L, "1호선", "파랑", + new HashMap<>(Map.of( + stationA, new Path(stationB, 5) + , stationB, new Path(stationC, 10) + )) + ); + + //when + final ShortestPath result = ShortestPathCalculator.calculate(stationA, stationC, of(line)); + final double distance = result.getDistance(); + + //then + assertThat(distance).isEqualTo(15); + } + + @DisplayName("최단 경로를 계산한다.") + @Test + void computeShortestPath2() { + //given + final Station stationA = new Station(1L, "A"); + final Station stationB = new Station(3L, "B"); + final Station stationC = new Station(2L, "C"); + final Line line = new Line(1L, "1호선", "파랑", + new HashMap<>(Map.of( + stationA, new Path(stationB, 5) + , stationB, new Path(stationC, 10) + )) + ); + + //when + final ShortestPath result = ShortestPathCalculator.calculate(stationA, stationC, of(line)); + final List way = result.getStations(); + + //then + assertThat(way).containsExactly(stationA, stationB, stationC); + } + + @DisplayName("환승을 포함한 최단 거리를 계산한다.") + @Test + void computeShortestPath3() { + //given + final Station stationA = new Station(1L, "A"); + final Station stationB = new Station(3L, "B"); + final Station stationC = new Station(2L, "C"); + final Line line = new Line(1L, "1호선", "파랑", + new HashMap<>(Map.of( + stationA, new Path(stationB, 10000) + , stationB, new Path(stationC, 10) + )) + ); + final Line line2 = new Line(2L, "3호선", "검정", + new HashMap<>(Map.of( + stationC, new Path(stationA, 5) + )) + ); + + //when + final ShortestPath result = ShortestPathCalculator.calculate(stationA, stationC, of(line, line2)); + final double distance = result.getDistance(); + + //then + assertThat(distance).isEqualTo(5); + } + + @DisplayName("최단 거리를 계산할 때 경로가 없다면 예외를 던진다.") + @Test + void computeShortestPathException() { + //given + final Station stationA = new Station(1L, "A"); + final Station stationB = new Station(3L, "B"); + final Station stationC = new Station(2L, "C"); + final Station stationD = new Station(4L, "D"); + final Line line = new Line(1L, "1호선", "파랑", + new HashMap<>(Map.of( + stationB, new Path(stationC, 10) + )) + ); + final Line line2 = new Line(2L, "3호선", "검정", + new HashMap<>(Map.of( + stationA, new Path(stationD, 5) + )) + ); + + //when,then + assertThatThrownBy(() -> ShortestPathCalculator.calculate(stationB, stationD, of(line, line2))) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/subway/fixture/LineFixture.java b/src/test/java/subway/fixture/LineFixture.java new file mode 100644 index 000000000..63bb4268b --- /dev/null +++ b/src/test/java/subway/fixture/LineFixture.java @@ -0,0 +1,47 @@ +package subway.fixture; + +import java.util.List; + +import static subway.fixture.StationFixture.선릉; +import static subway.fixture.StationFixture.수원; +import static subway.fixture.StationFixture.의왕; +import static subway.fixture.StationFixture.잠실나루; + +public enum LineFixture { + + 일호선(1L, "1호선", "파랑", List.of(수원, 잠실나루)), + 이호선(2L, "2호선", "초록", List.of(의왕, 수원, 선릉)), + 빈호선(3L, "empty", "none", List.of()); + + private final Long id; + private final String name; + private final String color; + private final List stationFixtures; + + LineFixture(final Long id, final String name, final String color, final List stationFixtures) { + this.id = id; + this.name = name; + this.color = color; + this.stationFixtures = stationFixtures; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public List getStationFixtures() { + return stationFixtures; + } + + public int getSize() { + return stationFixtures.size(); + } +} diff --git a/src/test/java/subway/fixture/StationFixture.java b/src/test/java/subway/fixture/StationFixture.java new file mode 100644 index 000000000..14ab9aa25 --- /dev/null +++ b/src/test/java/subway/fixture/StationFixture.java @@ -0,0 +1,26 @@ +package subway.fixture; + +public enum StationFixture { + + 수원(1L, "수원"), + 잠실나루(2L, "잠실나루"), + 의왕(3L, "의왕"), + 선릉(4L, "선릉"), + 여긴못감(5L, "여긴 못감"); + + private final Long id; + private final String name; + + StationFixture(final Long id, final String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/test/java/subway/integration/AcceptanceTest.java b/src/test/java/subway/integration/AcceptanceTest.java new file mode 100644 index 000000000..58a400054 --- /dev/null +++ b/src/test/java/subway/integration/AcceptanceTest.java @@ -0,0 +1,57 @@ +package subway.integration; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; + +@Sql({"/scheme.sql", "/data.sql"}) +@Sql(value = "/truncate.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public abstract class AcceptanceTest { + + @Autowired + protected ObjectMapper objectMapper; + + @LocalServerPort + private int port; + + @BeforeEach + public void setUp() { + RestAssured.port = port; + } + + protected ExtractableResponse httpGetRequest(final String url) { + return RestAssured.given().log().all() + .when() + .get(url) + .then().log().all() + .extract(); + } + + protected ExtractableResponse httpPostRequest(final String url, final Object body) throws JsonProcessingException { + final String json = objectMapper.writeValueAsString(body); + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(json) + .when() + .post(url) + .then().log().all() + .extract(); + } + + protected ExtractableResponse httpDeleteRequest(final String url) { + return RestAssured.given().log().all() + .when() + .delete(url) + .then().log().all() + .extract(); + } +} diff --git a/src/test/java/subway/integration/FeeAcceptanceTest.java b/src/test/java/subway/integration/FeeAcceptanceTest.java new file mode 100644 index 000000000..1e40ac145 --- /dev/null +++ b/src/test/java/subway/integration/FeeAcceptanceTest.java @@ -0,0 +1,45 @@ +package subway.integration; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import subway.dto.response.ShortestPathResponse; +import subway.fixture.StationFixture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static subway.fixture.StationFixture.수원; +import static subway.fixture.StationFixture.여긴못감; +import static subway.fixture.StationFixture.잠실나루; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class FeeAcceptanceTest extends AcceptanceTest { + + @Test + void 최단_거리를_조회한다() { + final var response = 요금을_조회한다(수원, 잠실나루); + 요금이_일치한다(response, 1250); + } + + @Test + void 경로가_없는_두_역의_최단_거리를_조회한다() { + final var response = 요금을_조회한다(잠실나루, 여긴못감); + 요금_조회에_실패한다(response); + + } + + private ExtractableResponse 요금을_조회한다(final StationFixture start, final StationFixture end) { + return httpGetRequest(String.format("/fee?start=%s&end=%s", start.getId(), end.getId())); + } + + private void 요금이_일치한다(final ExtractableResponse response, final int fee) { + assertThat(response.as(ShortestPathResponse.class).getFee()).isEqualTo(fee); + } + + private void 요금_조회에_실패한다(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(BAD_REQUEST.value()); + } +} diff --git a/src/test/java/subway/integration/IntegrationTest.java b/src/test/java/subway/integration/IntegrationTest.java deleted file mode 100644 index c30949402..000000000 --- a/src/test/java/subway/integration/IntegrationTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package subway.integration; - -import io.restassured.RestAssured; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.test.annotation.DirtiesContext; - -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class IntegrationTest { - @LocalServerPort - int port; - - @BeforeEach - public void setUp() { - RestAssured.port = port; - } -} diff --git a/src/test/java/subway/integration/LineAcceptanceTest.java b/src/test/java/subway/integration/LineAcceptanceTest.java new file mode 100644 index 000000000..03544b2a1 --- /dev/null +++ b/src/test/java/subway/integration/LineAcceptanceTest.java @@ -0,0 +1,136 @@ +package subway.integration; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import subway.dto.request.LineRequest; +import subway.dto.response.LineWithStationResponse; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; +import static subway.fixture.LineFixture.빈호선; +import static subway.fixture.LineFixture.이호선; +import static subway.fixture.LineFixture.일호선; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@DisplayName("지하철 노선 관련 기능") +public class LineAcceptanceTest extends AcceptanceTest { + + @Test + void 노선을_추가한다() throws JsonProcessingException { + final LineRequest lineRequest = new LineRequest("추가된 노선", "검정색"); + final var response = 노선을_추가한다(lineRequest); + 노선_추가에_성공한다(response); + } + + @Test + void 기존에_있는_이름으로_노선을_추가한다() throws JsonProcessingException { + final LineRequest lineRequest = new LineRequest(일호선.getName(), "아무색"); + final var response = 노선을_추가한다(lineRequest); + 노선_추가에_실패한다(response); + } + + @Test + void 역이_있는_노선을_조회한다() { + final var response = 노선을_조회한다(일호선.getId()); + 역이_있는_노선_조회에_성공한다(response); + } + + @Test + void 역이_없는_노선을_조회한다() { + final var response = 노선을_조회한다(빈호선.getId()); + 역이_없는_노선_조회에_성공한다(response); + } + + @Test + void 모든_역들을_조회한다() { + final var response = 모든_노선을_조회한다(); + 세개의_노선이_있다(response); + } + + @Test + void 노선을_삭제한다() { + final var response = 노선을_삭제한다(일호선.getId()); + final var response2 = 모든_노선을_조회한다(); + 노선_삭제에_성공한다(response, response2); + } + + @Test + void 없는_노선_삭제를_시도한다() { + final var response = 노선을_삭제한다(Long.MAX_VALUE); + 노선_삭제에_실패한다(response); + } + + private ExtractableResponse 노선을_조회한다(final Long id) { + return httpGetRequest("/lines/" + id); + } + + private ExtractableResponse 노선을_추가한다(final LineRequest request) throws JsonProcessingException { + return httpPostRequest("/lines", request); + } + + private List 모든_노선을_조회한다() { + return httpGetRequest("/lines").jsonPath().getList(".", LineWithStationResponse.class); + } + + private ExtractableResponse 노선을_삭제한다(final Long id) { + return httpDeleteRequest(String.format("/lines/%s", id)); + } + + private void 노선_추가에_성공한다(final ExtractableResponse response) { + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(CREATED.value()), + () -> assertThat(response.header("Location")).isNotNull() + ); + } + + private void 노선_추가에_실패한다(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(BAD_REQUEST.value()); + } + + private void 역이_있는_노선_조회에_성공한다(final ExtractableResponse response) { + assertAll( + () -> assertThat(response.as(LineWithStationResponse.class).getName()).isEqualTo(일호선.getName()), + () -> assertThat(response.as(LineWithStationResponse.class).getStations()).hasSize(일호선.getSize()), + () -> assertThat(response.statusCode()).isEqualTo(OK.value()) + ); + } + + private void 역이_없는_노선_조회에_성공한다(final ExtractableResponse response) { + assertAll( + () -> assertThat(response.as(LineWithStationResponse.class).getStations()).hasSize(빈호선.getSize()), + () -> assertThat(response.as(LineWithStationResponse.class).getName()).isEqualTo(빈호선.getName()) + ); + } + + private void 세개의_노선이_있다(final List response) { + assertAll( + () -> assertThat(response).hasSize(3), + () -> assertThat(response).extracting("name") + .containsExactly(일호선.getName(), 이호선.getName(), 빈호선.getName()) + ); + } + + private void 노선_삭제에_성공한다(final ExtractableResponse response, final List response2) { + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(NO_CONTENT.value()), + () -> assertThat(response2).extracting("name").doesNotContain(일호선.getName()) + ); + } + + private void 노선_삭제에_실패한다(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(NOT_FOUND.value()); + } +} diff --git a/src/test/java/subway/integration/LineIntegrationTest.java b/src/test/java/subway/integration/LineIntegrationTest.java deleted file mode 100644 index 0cb815f78..000000000 --- a/src/test/java/subway/integration/LineIntegrationTest.java +++ /dev/null @@ -1,137 +0,0 @@ -package subway.integration; - -import io.restassured.RestAssured; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import subway.dto.LineRequest; -import subway.dto.LineWithStationResponse; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("지하철 노선 관련 기능") -public class LineIntegrationTest extends IntegrationTest { - private LineRequest lineRequest1; - private LineRequest lineRequest2; - - @BeforeEach - public void setUp() { - super.setUp(); - - lineRequest1 = new LineRequest("신분당선", "bg-red-600"); - lineRequest2 = new LineRequest("구신분당선", "bg-red-600"); - } - - @DisplayName("지하철 노선을 생성한다.") - @Test - void createLine() { - // when - ExtractableResponse response = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.header("Location")).isNotBlank(); - } - - @DisplayName("기존에 존재하는 지하철 노선 이름으로 지하철 노선을 생성한다.") - @Test - void createLineWithDuplicateName() { - // given - RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // when - ExtractableResponse response = RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(lineRequest1) - .when().post("/lines") - .then().log().all(). - extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("지하철 노선을 조회한다.") - @Test - void getLine() { - // when - ExtractableResponse response = RestAssured - .given().log().all() - .accept(MediaType.APPLICATION_JSON_VALUE) - .when().get("/lines/2") - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - final LineWithStationResponse lineWithStationResponse = response.as(LineWithStationResponse.class); - assertThat(lineWithStationResponse.getId()).isEqualTo(2); - } - - @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/PathAcceptanceTest.java b/src/test/java/subway/integration/PathAcceptanceTest.java new file mode 100644 index 000000000..2130fa415 --- /dev/null +++ b/src/test/java/subway/integration/PathAcceptanceTest.java @@ -0,0 +1,111 @@ +package subway.integration; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import subway.dto.request.AddPathRequest; +import subway.dto.response.LineWithStationResponse; +import subway.fixture.LineFixture; +import subway.fixture.StationFixture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.http.HttpStatus.OK; +import static subway.fixture.LineFixture.이호선; +import static subway.fixture.LineFixture.일호선; +import static subway.fixture.StationFixture.선릉; +import static subway.fixture.StationFixture.수원; +import static subway.fixture.StationFixture.의왕; +import static subway.fixture.StationFixture.잠실나루; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class PathAcceptanceTest extends AcceptanceTest { + + @Test + void 역이_두개만_있다면_동시에_삭제된다() { + final var response = 노선의_역을_삭제한다(일호선, 수원); + final var response2 = 노선을_조회한다(일호선); + + 역_두개가_모두_삭제된다(response, response2); + } + + @Test + void 역이_세개있는_노선의_마지막_역을_삭제한다() { + final var response = 노선의_역을_삭제한다(이호선, 선릉); + final var response2 = 노선을_조회한다(이호선); + + 마지막_노선_삭제에_성공한다(response, response2); + } + + @Test + void 노선_경로에_하행역을_추가한다() throws JsonProcessingException { + final AddPathRequest request = new AddPathRequest(잠실나루.getId(), 선릉.getId(), 10, "down"); + final var response = 노선에_역을_추가한다(request, 일호선); + final var response2 = 노선을_조회한다(일호선); + + 하행역_추가에_성공한다(response, response2); + } + + @Test + void 노선_경로에_상행역을_추가한다() throws JsonProcessingException { + final AddPathRequest request = new AddPathRequest(잠실나루.getId(), 선릉.getId(), 3, "up"); + final var response = 노선에_역을_추가한다(request, 일호선); + final var response2 = 노선을_조회한다(일호선); + + 상행역_추가에_성공한다(response, response2); + } + + private ExtractableResponse 노선의_역을_삭제한다(final LineFixture line, final StationFixture station) { + return httpDeleteRequest(String.format("/lines/%s/stations/%s", line.getId(), station.getId())); + } + + private ExtractableResponse 노선을_조회한다(final LineFixture line) { + return httpGetRequest(String.format("/lines/%s", line.getId())); + } + + private ExtractableResponse 노선에_역을_추가한다(final AddPathRequest request, final LineFixture line) throws JsonProcessingException { + return httpPostRequest(String.format("/lines/%s/stations/", line.getId()), request); + } + + private void 역_두개가_모두_삭제된다(final ExtractableResponse response, final ExtractableResponse response2) { + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(OK.value()), + () -> assertThat(response2.as(LineWithStationResponse.class).getStations()) + .hasSize(일호선.getSize() - 2) + ); + } + + private void 마지막_노선_삭제에_성공한다(final ExtractableResponse response, final ExtractableResponse response2) { + final LineWithStationResponse actualResponse = response2.as(LineWithStationResponse.class); + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(OK.value()), + () -> assertThat(actualResponse.getStations()).hasSize(이호선.getSize() - 1), + () -> assertThat(actualResponse.getStations()).extracting("name") + .containsExactly(의왕.getName(), 수원.getName()) + ); + } + + private void 하행역_추가에_성공한다(final ExtractableResponse response, final ExtractableResponse response2) { + final var actualResponse = response2.as(LineWithStationResponse.class); + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(OK.value()), + () -> assertThat(actualResponse.getStations()).hasSize(일호선.getSize() + 1), + () -> assertThat(actualResponse.getStations()).extracting("name") + .containsExactly(수원.getName(), 잠실나루.getName(), 선릉.getName()) + ); + } + + private void 상행역_추가에_성공한다(final ExtractableResponse response, final ExtractableResponse response2) { + final var actualResponse = response2.as(LineWithStationResponse.class); + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(OK.value()), + () -> assertThat(actualResponse.getStations()).hasSize(일호선.getSize() + 1), + () -> assertThat(actualResponse.getStations()).extracting("name") + .containsExactly(수원.getName(), 선릉.getName(), 잠실나루.getName()) + ); + } +} diff --git a/src/test/java/subway/integration/StationAcceptanceTest.java b/src/test/java/subway/integration/StationAcceptanceTest.java new file mode 100644 index 000000000..cbe09a158 --- /dev/null +++ b/src/test/java/subway/integration/StationAcceptanceTest.java @@ -0,0 +1,47 @@ +package subway.integration; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import subway.fixture.StationFixture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static subway.fixture.StationFixture.수원; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@DisplayName("지하철역 관련 기능") +public class StationAcceptanceTest extends AcceptanceTest { + + @Test + void 지하철_역을_삭제한다() { + final var response = 역을_제거한다(수원); + 역_제거에_성공한다(response); + } + + @Test + void 없는_역을_삭제한다() { + 역을_제거한다(수원); + + final var response = 역을_제거한다(수원); + 상태코드_404를_반환한다(response); + } + + private ExtractableResponse 역을_제거한다(final StationFixture station) { + final Long id = station.getId(); + return httpDeleteRequest(String.format("/stations/%s", id)); + } + + private void 역_제거에_성공한다(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(NO_CONTENT.value()); + } + + private void 상태코드_404를_반환한다(final ExtractableResponse response) { + assertThat(response.statusCode()).isEqualTo(NOT_FOUND.value()); + } +} diff --git a/src/test/java/subway/integration/StationIntegrationTest.java b/src/test/java/subway/integration/StationIntegrationTest.java deleted file mode 100644 index a97d184a0..000000000 --- a/src/test/java/subway/integration/StationIntegrationTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package subway.integration; - -import io.restassured.RestAssured; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import subway.dto.StationResponse; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("지하철역 관련 기능") -public class StationIntegrationTest extends IntegrationTest { - @DisplayName("지하철역을 생성한다.") - @Test - void createStation() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - - // when - ExtractableResponse response = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.header("Location")).isNotBlank(); - } - - @DisplayName("기존에 존재하는 지하철역 이름으로 지하철역을 생성한다.") - @Test - void createStationWithDuplicateName() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - ExtractableResponse response = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then() - .log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - @DisplayName("지하철역 목록을 조회한다.") - @Test - void getStations() { - /// given - Map params1 = new HashMap<>(); - params1.put("name", "강남역"); - ExtractableResponse createResponse1 = RestAssured.given().log().all() - .body(params1) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - Map params2 = new HashMap<>(); - params2.put("name", "역삼역"); - ExtractableResponse createResponse2 = RestAssured.given().log().all() - .body(params2) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - ExtractableResponse response = RestAssured.given().log().all() - .when() - .get("/stations") - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - List expectedStationIds = Stream.of(createResponse1, createResponse2) - .map(it -> Long.parseLong(it.header("Location").split("/")[2])) - .collect(Collectors.toList()); - List resultStationIds = response.jsonPath().getList(".", StationResponse.class).stream() - .map(StationResponse::getId) - .collect(Collectors.toList()); - assertThat(resultStationIds).containsAll(expectedStationIds); - } - - @DisplayName("지하철역을 조회한다.") - @Test - void getStation() { - /// given - Map params1 = new HashMap<>(); - params1.put("name", "강남역"); - ExtractableResponse createResponse = RestAssured.given().log().all() - .body(params1) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - Long stationId = Long.parseLong(createResponse.header("Location").split("/")[2]); - ExtractableResponse response = RestAssured.given().log().all() - .when() - .get("/stations/{stationId}", stationId) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - StationResponse stationResponse = response.as(StationResponse.class); - assertThat(stationResponse.getId()).isEqualTo(stationId); - } - - @DisplayName("지하철역을 수정한다.") - @Test - void updateStation() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - ExtractableResponse createResponse = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - Map otherParams = new HashMap<>(); - otherParams.put("name", "삼성역"); - String uri = createResponse.header("Location"); - ExtractableResponse response = RestAssured.given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(otherParams) - .when() - .put(uri) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - } - - @DisplayName("지하철역을 제거한다.") - @Test - void deleteStation() { - // given - Map params = new HashMap<>(); - params.put("name", "강남역"); - ExtractableResponse createResponse = RestAssured.given().log().all() - .body(params) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when() - .post("/stations") - .then().log().all() - .extract(); - - // when - String uri = createResponse.header("Location"); - ExtractableResponse response = RestAssured.given().log().all() - .when() - .delete(uri) - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); - } -} diff --git a/src/test/java/subway/integration/SubwayAcceptanceTest.java b/src/test/java/subway/integration/SubwayAcceptanceTest.java new file mode 100644 index 000000000..e6183e5d8 --- /dev/null +++ b/src/test/java/subway/integration/SubwayAcceptanceTest.java @@ -0,0 +1,66 @@ +package subway.integration; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import subway.dto.response.LineWithStationResponse; +import subway.fixture.LineFixture; +import subway.fixture.StationFixture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static subway.fixture.LineFixture.이호선; +import static subway.fixture.LineFixture.일호선; +import static subway.fixture.StationFixture.선릉; +import static subway.fixture.StationFixture.수원; +import static subway.fixture.StationFixture.의왕; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class SubwayAcceptanceTest extends AcceptanceTest { + + @Test + void 역을_제거하면_경로에서도_제거된다() { + 역을_제거한다(수원); + final var response = 노선을_조회한다(이호선); + 노선에서_제거된_것을_확인한다(response); + } + + @Test + void 환승역을_삭제한다() { + 노선의_역을_삭제한다(일호선, 수원); + final var response = 노선을_조회한다(이호선); + 환승역을_삭제해도_다른_노선에_영향이_없다(response); + } + + private ExtractableResponse 역을_제거한다(final StationFixture station) { + final Long id = station.getId(); + return httpDeleteRequest(String.format("/stations/%s", id)); + } + + private ExtractableResponse 노선을_조회한다(final LineFixture line) { + final Long id = line.getId(); + return httpGetRequest(String.format("/lines/%s", id)); + } + + private ExtractableResponse 노선의_역을_삭제한다(final LineFixture line, final StationFixture station) { + return httpDeleteRequest(String.format("/lines/%s/stations/%s", line.getId(), station.getId())); + } + + private void 노선에서_제거된_것을_확인한다(final ExtractableResponse response) { + final var actualResponse = response.as(LineWithStationResponse.class); + assertAll( + () -> assertThat(actualResponse.getStations()).hasSize(이호선.getSize() - 1), + () -> assertThat(actualResponse.getStations()).extracting("name") + .doesNotContain(수원.getName()) + ); + } + + private void 환승역을_삭제해도_다른_노선에_영향이_없다(final ExtractableResponse response) { + final var actualResponse = response.as(LineWithStationResponse.class); + assertThat(actualResponse.getStations()).extracting("name") + .containsExactly(의왕.getName(), 수원.getName(), 선릉.getName()); + } +} diff --git a/src/test/java/subway/persistence/dao/DaoTest.java b/src/test/java/subway/persistence/dao/DaoTest.java new file mode 100644 index 000000000..705c78772 --- /dev/null +++ b/src/test/java/subway/persistence/dao/DaoTest.java @@ -0,0 +1,31 @@ +package subway.persistence.dao; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.jdbc.Sql; +import subway.persistence.repository.SubwayRepository; + + +@Sql({"/scheme.sql", "/data.sql"}) +@Sql(value = "/truncate.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +@JdbcTest +public abstract class DaoTest { + + @Autowired + protected JdbcTemplate jdbcTemplate; + + protected LineDao lineDao; + protected StationDao stationDao; + protected PathDao pathDao; + protected SubwayRepository subwayRepository; + + @BeforeEach + void setUp() { + this.lineDao = new LineDao(jdbcTemplate); + this.stationDao = new StationDao(jdbcTemplate); + this.pathDao = new PathDao(jdbcTemplate); + this.subwayRepository = new SubwayRepository(lineDao, stationDao, pathDao); + } +} diff --git a/src/test/java/subway/persistence/dao/LineDaoTest.java b/src/test/java/subway/persistence/dao/LineDaoTest.java new file mode 100644 index 000000000..762bfcfd4 --- /dev/null +++ b/src/test/java/subway/persistence/dao/LineDaoTest.java @@ -0,0 +1,55 @@ +package subway.persistence.dao; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import subway.domain.Line; +import subway.exception.LineNotFoundException; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LineDaoTest extends DaoTest { + + @Test + void 노선을_조회한다() { + final Line line = lineDao.findById(1L); + + assertThat(line.getName()).isEqualTo("1호선"); + } + + @Test + void 노선을_추가한다() { + final Line insert = lineDao.insert(new Line("추가노선", "검정")); + final Line line = lineDao.findById(insert.getId()); + + Assertions.assertAll( + () -> assertThat(line.getName()).isEqualTo("추가노선"), + () -> assertThat(line.getColor()).isEqualTo("검정") + ); + } + + @Test + void 모든_노선을_조회한다() { + final List lines = lineDao.findAll(); + + assertThat(lines).hasSize(3); + } + + @Test + void 노선을_삭제한다() { + lineDao.deleteById(1L); + + final List lines = lineDao.findAll(); + + assertThat(lines).hasSize(2); + } + + @Test + void 없는_노선을_삭제한다() { + assertThatThrownBy(() -> lineDao.deleteById(10L)) + .isInstanceOf(LineNotFoundException.class) + .hasMessageContaining("존재하지 않는 노선입니다."); + } +} diff --git a/src/test/java/subway/persistence/dao/PathDaoTest.java b/src/test/java/subway/persistence/dao/PathDaoTest.java new file mode 100644 index 000000000..2bb5a7555 --- /dev/null +++ b/src/test/java/subway/persistence/dao/PathDaoTest.java @@ -0,0 +1,26 @@ +package subway.persistence.dao; + +import org.junit.jupiter.api.Test; +import subway.persistence.dao.entity.PathEntity; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class PathDaoTest extends DaoTest { + + @Test + void 모든_경로를_조회한다() { + final List paths = pathDao.findAll(); + + assertThat(paths).hasSize(3); + } + + @Test + void 노선의_모든_경로를_삭제한다() { + pathDao.deleteByLineId(2L); + + assertThat(pathDao.findAll()).hasSize(1); + } + +} diff --git a/src/test/java/subway/persistence/dao/StationDaoTest.java b/src/test/java/subway/persistence/dao/StationDaoTest.java new file mode 100644 index 000000000..86af605b2 --- /dev/null +++ b/src/test/java/subway/persistence/dao/StationDaoTest.java @@ -0,0 +1,63 @@ +package subway.persistence.dao; + +import org.junit.jupiter.api.Test; +import subway.domain.Station; +import subway.exception.StationNotFoundException; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class StationDaoTest extends DaoTest { + + @Test + void 역을_조회한다() { + final Station station = stationDao.findById(1L); + + assertThat(station).isEqualTo(new Station(1L, "수원")); + } + + @Test + void 역을_추가한다() { + final Station insert = stationDao.insert(new Station("추가")); + final Long id = insert.getId(); + final Station station = stationDao.findById(id); + + assertThat(insert).isEqualTo(station); + } + + @Test + void 역을_수정한다() { + stationDao.update(new Station(1L, "수정")); + + final Station station = stationDao.findById(1L); + + assertThat(station).isEqualTo(new Station(1L, "수정")); + } + + @Test + void 모든_역을_조회한다() { + final List all = stationDao.findAll(); + + assertThat(all).hasSize(5); + } + + @Test + void 역을_삭제한다() { + stationDao.deleteById(1L); + + final List all = stationDao.findAll(); + + assertThat(all).hasSize(4); + } + + @Test + void 없는_역을_삭제한다() { + stationDao.deleteById(1L); + assertThatThrownBy(() -> stationDao.deleteById(1L)) + .isInstanceOf(StationNotFoundException.class) + .hasMessageContaining("존재하지 않는 역입니다."); + } + +} diff --git a/src/test/java/subway/persistence/repository/SubwayRepositoryTest.java b/src/test/java/subway/persistence/repository/SubwayRepositoryTest.java new file mode 100644 index 000000000..a44db80ce --- /dev/null +++ b/src/test/java/subway/persistence/repository/SubwayRepositoryTest.java @@ -0,0 +1,55 @@ +package subway.persistence.repository; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import subway.domain.Line; +import subway.domain.Station; +import subway.exception.DuplicatedNameException; +import subway.persistence.dao.DaoTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SubwayRepositoryTest extends DaoTest { + + @Test + void 노선을_조회한다() { + final Line line = subwayRepository.findLine(1L); + + Assertions.assertAll( + () -> assertThat(line.getName()).isEqualTo("1호선"), + () -> assertThat(line.getColor()).isEqualTo("파랑"), + () -> assertThat(line.getPaths()).hasSize(1) + ); + } + + @Test + void 모든_노선을_조회한다() { + final List lines = subwayRepository.findLines(); + + assertThat(lines).hasSize(3); + } + + @Test + void 노선을_삭제한다() { + subwayRepository.deleteLineById(1L); + + final List lines = subwayRepository.findLines(); + + assertThat(lines).hasSize(2); + } + + @Test + void 중복된_이름의_노선을_추가한다() { + assertThatThrownBy(() -> subwayRepository.addLine(new Line("1호선", "아무색"))) + .isInstanceOf(DuplicatedNameException.class); + } + + @Test + void 중복된_이름의_역을_추가한다() { + assertThatThrownBy(() -> subwayRepository.addStation(new Station("수원"))) + .isInstanceOf(DuplicatedNameException.class); + } +} diff --git a/src/test/java/subway/support/DijkstraTest.java b/src/test/java/subway/support/DijkstraTest.java new file mode 100644 index 000000000..62d2eb900 --- /dev/null +++ b/src/test/java/subway/support/DijkstraTest.java @@ -0,0 +1,26 @@ +package subway.support; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class DijkstraTest { + + @Test + void 최단_경로를_계산한다() { + //given + final Dijkstra dijkstra = new Dijkstra<>(); + List from = List.of("1", "2", "3"); + List to = List.of("3", "4", "4"); + List distance = List.of(1, 2, 3); + + //when + final double weight = dijkstra.shortestPath(from, to, distance, "1", "4").getWeight(); + + //then + assertThat(weight).isEqualTo(4.0); + } + +} diff --git a/src/test/java/subway/ui/ControllerTest.java b/src/test/java/subway/ui/ControllerTest.java new file mode 100644 index 000000000..8ece0f579 --- /dev/null +++ b/src/test/java/subway/ui/ControllerTest.java @@ -0,0 +1,29 @@ +package subway.ui; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.test.web.servlet.MockMvc; +import subway.application.service.FeeService; +import subway.application.service.LineService; +import subway.application.service.PathService; + +@WebMvcTest({LineController.class, PathController.class, FeeController.class}) +public abstract class ControllerTest { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + protected LineService lineService; + + @MockBean + protected PathService pathService; + + @MockBean + protected FeeService feeService; +} diff --git a/src/test/java/subway/ui/FeeControllerTest.java b/src/test/java/subway/ui/FeeControllerTest.java new file mode 100644 index 000000000..8ac9ea920 --- /dev/null +++ b/src/test/java/subway/ui/FeeControllerTest.java @@ -0,0 +1,30 @@ +package subway.ui; + +import org.junit.jupiter.api.Test; +import subway.dto.response.ShortestPathResponse; +import subway.dto.response.StationResponse; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class FeeControllerTest extends ControllerTest { + + @Test + void 요금을_조회한다() throws Exception { + given(feeService.showShortestPath(anyLong(), anyLong())) + .willReturn(new ShortestPathResponse(1250, List.of( + new StationResponse(1L, "1번역"), + new StationResponse(2L, "2번역") + ))); + + mockMvc.perform(get("/fee?start=1&end=2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.fee", is(1250))); + } +} diff --git a/src/test/java/subway/ui/LineControllerTest.java b/src/test/java/subway/ui/LineControllerTest.java index 5404b47fe..4be7df0bf 100644 --- a/src/test/java/subway/ui/LineControllerTest.java +++ b/src/test/java/subway/ui/LineControllerTest.java @@ -2,14 +2,10 @@ 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.LineService; -import subway.dto.LineWithStationResponse; -import subway.dto.StationResponse; +import subway.dto.response.LineWithStationResponse; +import subway.dto.response.StationResponse; import java.util.List; @@ -22,13 +18,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(LineController.class) -class LineControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private LineService lineService; +class LineControllerTest extends ControllerTest { @DisplayName("노선을 조회해서 역을 순서대로 보여준다.") @Test diff --git a/src/test/java/subway/ui/PathControllerTest.java b/src/test/java/subway/ui/PathControllerTest.java index c629fa28f..e605ca2ab 100644 --- a/src/test/java/subway/ui/PathControllerTest.java +++ b/src/test/java/subway/ui/PathControllerTest.java @@ -1,30 +1,14 @@ package subway.ui; -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 org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import subway.application.PathService; -import subway.dto.AddPathRequest; +import subway.dto.request.AddPathRequest; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(PathController.class) -class PathControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @MockBean - private PathService pathService; +class PathControllerTest extends ControllerTest { @DisplayName("노선의 경로에 역을 추가한다.") @Test diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 000000000..8635e99f7 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,3 @@ +spring.datasource.url=jdbc:h2:mem:testdb;MODE=mysql +spring.sql.init.mode=never + diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 000000000..dc2d246a8 --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,16 @@ +INSERT INTO line (name, color) +VALUES ('1호선', '파랑'), + ('2호선', '초록'), + ('empty', 'none'); + +INSERT INTO station (name) +VALUES ('수원'), + ('잠실나루'), + ('의왕'), + ('선릉'), + ('여긴 못감'); + +INSERT INTO paths (line_id, up_station_id, down_station_id, distance) +VALUES (1, 1, 2, 5), + (2, 3, 1, 5), + (2, 1, 4, 7); diff --git a/src/test/resources/logback-access.xml b/src/test/resources/logback-access.xml new file mode 100644 index 000000000..38e0823f4 --- /dev/null +++ b/src/test/resources/logback-access.xml @@ -0,0 +1,8 @@ + + + + %n###### HTTP Request ######%n%fullRequest%n###### HTTP Response ######%n%fullResponse%n%n + + + + \ No newline at end of file diff --git a/src/test/resources/scheme.sql b/src/test/resources/scheme.sql new file mode 100644 index 000000000..1ad04ebd5 --- /dev/null +++ b/src/test/resources/scheme.sql @@ -0,0 +1,25 @@ +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 paths +( + id BIGINT AUTO_INCREMENT NOT NULL, + line_id BIGINT NOT NULL, + up_station_id BIGINT NOT NULL, + down_station_id BIGINT NOT NULL, + distance INT NOT NULL, + PRIMARY KEY(id) +); + diff --git a/src/test/resources/truncate.sql b/src/test/resources/truncate.sql new file mode 100644 index 000000000..751bd374e --- /dev/null +++ b/src/test/resources/truncate.sql @@ -0,0 +1,3 @@ +TRUNCATE TABLE paths RESTART IDENTITY; +TRUNCATE TABLE line RESTART IDENTITY; +TRUNCATE TABLE station RESTART IDENTITY;