From af360eeba9ea78beadb231330871c44bedeae264 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 14:10:03 +0900 Subject: [PATCH 01/28] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../subway/controller/LineController.java | 61 ++++ .../subway/controller/SectionController.java | 37 +++ .../subway/controller/StationController.java | 44 +++ .../controller/SubwayControllerAdvice.java | 51 ++++ .../java/wooteco/subway/dao/StationDao.java | 30 -- .../subway/dao/line/InmemoryLineDao.java | 78 +++++ .../wooteco/subway/dao/line/JdbcLineDao.java | 77 +++++ .../java/wooteco/subway/dao/line/LineDao.java | 21 ++ .../dao/section/InmemorySectionDao.java | 66 +++++ .../subway/dao/section/JdbcSectionDao.java | 86 ++++++ .../subway/dao/section/SectionDao.java | 15 + .../dao/station/InmemoryStationDao.java | 72 +++++ .../subway/dao/station/JdbcStationDao.java | 69 +++++ .../subway/dao/station/StationDao.java | 19 ++ src/main/java/wooteco/subway/domain/Line.java | 53 ++++ .../java/wooteco/subway/domain/Section.java | 128 ++++++++ .../java/wooteco/subway/domain/Sections.java | 256 ++++++++++++++++ .../java/wooteco/subway/domain/Station.java | 32 +- .../java/wooteco/subway/dto/LineRequest.java | 40 --- .../java/wooteco/subway/dto/LineResponse.java | 33 --- .../wooteco/subway/dto/SectionRequest.java | 28 -- .../wooteco/subway/dto/StationRequest.java | 22 -- .../subway/dto/SubwayErrorResponse.java | 35 +++ .../wooteco/subway/dto/line/LineResponse.java | 48 +++ .../subway/dto/line/LineSaveRequest.java | 59 ++++ .../subway/dto/line/LineUpdateRequest.java | 33 +++ .../dto/section/SectionSaveRequest.java | 36 +++ .../dto/{ => station}/StationResponse.java | 14 +- .../dto/station/StationSaveRequest.java | 25 ++ .../subway/exception/NotFoundException.java | 8 + .../wooteco/subway/service/LineService.java | 82 +++++ .../subway/service/SectionService.java | 55 ++++ .../subway/service/StationService.java | 53 ++++ .../wooteco/subway/ui/StationController.java | 38 --- src/main/resources/application.yml | 9 + src/main/resources/schema.sql | 28 ++ .../subway/acceptance/AcceptanceTest.java | 7 +- .../subway/acceptance/LineAcceptanceTest.java | 190 ++++++++++++ .../acceptance/SectionAcceptanceTest.java | 92 ++++++ .../acceptance/StationAcceptanceTest.java | 148 ++++------ .../SubwayControllerAdviceTest.java | 43 +++ .../subway/dao/line/InmemoryLineDaoTest.java | 78 +++++ .../subway/dao/line/JdbcLineDaoTest.java | 85 ++++++ .../dao/section/InmemorySectionDaoTest.java | 66 +++++ .../dao/section/JdbcSectionDaoTest.java | 94 ++++++ .../dao/station/InmemoryStationDaoTest.java | 69 +++++ .../dao/station/JdbcStationDaoTest.java | 75 +++++ .../wooteco/subway/domain/SectionTest.java | 193 ++++++++++++ .../wooteco/subway/domain/SectionsTest.java | 279 ++++++++++++++++++ .../subway/service/LineServiceTest.java | 66 +++++ .../subway/service/SectionServiceTest.java | 70 +++++ .../subway/service/StationServiceTest.java | 38 +++ src/test/resources/application.yml | 9 + src/test/resources/schema-truncate.sql | 3 + src/test/resources/schema.sql | 28 ++ 56 files changed, 3183 insertions(+), 292 deletions(-) create mode 100644 src/main/java/wooteco/subway/controller/LineController.java create mode 100644 src/main/java/wooteco/subway/controller/SectionController.java create mode 100644 src/main/java/wooteco/subway/controller/StationController.java create mode 100644 src/main/java/wooteco/subway/controller/SubwayControllerAdvice.java delete mode 100644 src/main/java/wooteco/subway/dao/StationDao.java create mode 100644 src/main/java/wooteco/subway/dao/line/InmemoryLineDao.java create mode 100644 src/main/java/wooteco/subway/dao/line/JdbcLineDao.java create mode 100644 src/main/java/wooteco/subway/dao/line/LineDao.java create mode 100644 src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java create mode 100644 src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java create mode 100644 src/main/java/wooteco/subway/dao/section/SectionDao.java create mode 100644 src/main/java/wooteco/subway/dao/station/InmemoryStationDao.java create mode 100644 src/main/java/wooteco/subway/dao/station/JdbcStationDao.java create mode 100644 src/main/java/wooteco/subway/dao/station/StationDao.java create mode 100644 src/main/java/wooteco/subway/domain/Line.java create mode 100644 src/main/java/wooteco/subway/domain/Section.java create mode 100644 src/main/java/wooteco/subway/domain/Sections.java delete mode 100644 src/main/java/wooteco/subway/dto/LineRequest.java delete mode 100644 src/main/java/wooteco/subway/dto/LineResponse.java delete mode 100644 src/main/java/wooteco/subway/dto/SectionRequest.java delete mode 100644 src/main/java/wooteco/subway/dto/StationRequest.java create mode 100644 src/main/java/wooteco/subway/dto/SubwayErrorResponse.java create mode 100644 src/main/java/wooteco/subway/dto/line/LineResponse.java create mode 100644 src/main/java/wooteco/subway/dto/line/LineSaveRequest.java create mode 100644 src/main/java/wooteco/subway/dto/line/LineUpdateRequest.java create mode 100644 src/main/java/wooteco/subway/dto/section/SectionSaveRequest.java rename src/main/java/wooteco/subway/dto/{ => station}/StationResponse.java (53%) create mode 100644 src/main/java/wooteco/subway/dto/station/StationSaveRequest.java create mode 100644 src/main/java/wooteco/subway/exception/NotFoundException.java create mode 100644 src/main/java/wooteco/subway/service/LineService.java create mode 100644 src/main/java/wooteco/subway/service/SectionService.java create mode 100644 src/main/java/wooteco/subway/service/StationService.java delete mode 100644 src/main/java/wooteco/subway/ui/StationController.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/schema.sql create mode 100644 src/test/java/wooteco/subway/acceptance/LineAcceptanceTest.java create mode 100644 src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java create mode 100644 src/test/java/wooteco/subway/controller/SubwayControllerAdviceTest.java create mode 100644 src/test/java/wooteco/subway/dao/line/InmemoryLineDaoTest.java create mode 100644 src/test/java/wooteco/subway/dao/line/JdbcLineDaoTest.java create mode 100644 src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java create mode 100644 src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java create mode 100644 src/test/java/wooteco/subway/dao/station/InmemoryStationDaoTest.java create mode 100644 src/test/java/wooteco/subway/dao/station/JdbcStationDaoTest.java create mode 100644 src/test/java/wooteco/subway/domain/SectionTest.java create mode 100644 src/test/java/wooteco/subway/domain/SectionsTest.java create mode 100644 src/test/java/wooteco/subway/service/LineServiceTest.java create mode 100644 src/test/java/wooteco/subway/service/SectionServiceTest.java create mode 100644 src/test/java/wooteco/subway/service/StationServiceTest.java create mode 100644 src/test/resources/application.yml create mode 100644 src/test/resources/schema-truncate.sql create mode 100644 src/test/resources/schema.sql diff --git a/build.gradle b/build.gradle index 4e745d6b7..e4cf36a29 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ dependencies { // spring implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-validation' // log implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' diff --git a/src/main/java/wooteco/subway/controller/LineController.java b/src/main/java/wooteco/subway/controller/LineController.java new file mode 100644 index 000000000..741a13c56 --- /dev/null +++ b/src/main/java/wooteco/subway/controller/LineController.java @@ -0,0 +1,61 @@ +package wooteco.subway.controller; + +import java.net.URI; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.Positive; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wooteco.subway.dto.line.LineResponse; +import wooteco.subway.dto.line.LineSaveRequest; +import wooteco.subway.dto.line.LineUpdateRequest; +import wooteco.subway.service.LineService; + +@RestController +@RequestMapping("/lines") +public class LineController { + + private final LineService lineService; + + public LineController(final LineService lineService) { + this.lineService = lineService; + } + + @PostMapping + public ResponseEntity createLine(@RequestBody @Valid LineSaveRequest lineSaveRequest) { + LineResponse response = lineService.save(lineSaveRequest); + return ResponseEntity.created(URI.create("/lines/" + response.getId())).body(response); + } + + @GetMapping + public ResponseEntity> showLines() { + return ResponseEntity.ok().body(lineService.findAll()); + } + + @GetMapping("/{lineId}") + public ResponseEntity showLine( + @PathVariable @Positive(message = "노선의 id는 양수 값만 들어올 수 있습니다.") Long lineId) { + LineResponse response = lineService.findById(lineId); + return ResponseEntity.ok().body(response); + } + + @PutMapping("/{lineId}") + public ResponseEntity updateLine(@PathVariable Long lineId, + @RequestBody @Valid LineUpdateRequest lineUpdateRequest) { + lineService.update(lineId, lineUpdateRequest); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{lineId}") + public ResponseEntity deleteLine(@PathVariable Long lineId) { + lineService.delete(lineId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/wooteco/subway/controller/SectionController.java b/src/main/java/wooteco/subway/controller/SectionController.java new file mode 100644 index 000000000..ed510e024 --- /dev/null +++ b/src/main/java/wooteco/subway/controller/SectionController.java @@ -0,0 +1,37 @@ +package wooteco.subway.controller; + +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import wooteco.subway.dto.section.SectionSaveRequest; +import wooteco.subway.service.SectionService; + +@RestController +@RequestMapping("/lines/{lineId}/sections") +public class SectionController { + + private final SectionService sectionService; + + public SectionController(final SectionService sectionService) { + this.sectionService = sectionService; + } + + @PostMapping + public ResponseEntity saveSection(@PathVariable long lineId, + @RequestBody @Valid SectionSaveRequest sectionSaveRequest) { + sectionService.save(lineId, sectionSaveRequest); + return ResponseEntity.ok().build(); + } + + @DeleteMapping + public ResponseEntity deleteSection(@PathVariable long lineId, @RequestParam long stationId) { + sectionService.delete(lineId, stationId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/wooteco/subway/controller/StationController.java b/src/main/java/wooteco/subway/controller/StationController.java new file mode 100644 index 000000000..10f45fd17 --- /dev/null +++ b/src/main/java/wooteco/subway/controller/StationController.java @@ -0,0 +1,44 @@ +package wooteco.subway.controller; + +import java.net.URI; +import java.util.List; +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wooteco.subway.dto.station.StationResponse; +import wooteco.subway.dto.station.StationSaveRequest; +import wooteco.subway.service.StationService; + +@RestController +@RequestMapping("/stations") +public class StationController { + + private final StationService stationService; + + public StationController(final StationService stationService) { + this.stationService = stationService; + } + + @PostMapping + public ResponseEntity createStation(@RequestBody @Valid StationSaveRequest stationSaveRequest) { + StationResponse response = stationService.save(stationSaveRequest.toStation()); + return ResponseEntity.created(URI.create("/stations/" + response.getId())).body(response); + } + + @GetMapping + public ResponseEntity> showStations() { + return ResponseEntity.ok().body(stationService.findAll()); + } + + @DeleteMapping("/{stationId}") + public ResponseEntity deleteStation(@PathVariable Long stationId) { + stationService.delete(stationId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/wooteco/subway/controller/SubwayControllerAdvice.java b/src/main/java/wooteco/subway/controller/SubwayControllerAdvice.java new file mode 100644 index 000000000..312e50928 --- /dev/null +++ b/src/main/java/wooteco/subway/controller/SubwayControllerAdvice.java @@ -0,0 +1,51 @@ +package wooteco.subway.controller; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; +import wooteco.subway.dto.SubwayErrorResponse; +import wooteco.subway.exception.NotFoundException; + +@RestControllerAdvice +public class SubwayControllerAdvice { + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleBusinessException(IllegalStateException exception) { + return ResponseEntity.badRequest().body(SubwayErrorResponse.from(exception)); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodValidException(MethodArgumentNotValidException exception) { + return ResponseEntity.badRequest().body(SubwayErrorResponse.from(exception)); + } + + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity handleNoHandlerFoundException() { + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException exception) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(SubwayErrorResponse.from(exception)); + } + + @ExceptionHandler(EmptyResultDataAccessException.class) + public ResponseEntity handleNotFoundException() { + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler(DataAccessException.class) + public ResponseEntity handleDataAccessException() { + return ResponseEntity.internalServerError().build(); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException() { + return ResponseEntity.internalServerError().build(); + } +} diff --git a/src/main/java/wooteco/subway/dao/StationDao.java b/src/main/java/wooteco/subway/dao/StationDao.java deleted file mode 100644 index 85e2cba53..000000000 --- a/src/main/java/wooteco/subway/dao/StationDao.java +++ /dev/null @@ -1,30 +0,0 @@ -package wooteco.subway.dao; - -import org.springframework.util.ReflectionUtils; -import wooteco.subway.domain.Station; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - -public class StationDao { - private static Long seq = 0L; - private static List stations = new ArrayList<>(); - - public static Station save(Station station) { - Station persistStation = createNewObject(station); - stations.add(persistStation); - return persistStation; - } - - public static List findAll() { - return stations; - } - - private static Station createNewObject(Station station) { - Field field = ReflectionUtils.findField(Station.class, "id"); - field.setAccessible(true); - ReflectionUtils.setField(field, station, ++seq); - return station; - } -} \ No newline at end of file diff --git a/src/main/java/wooteco/subway/dao/line/InmemoryLineDao.java b/src/main/java/wooteco/subway/dao/line/InmemoryLineDao.java new file mode 100644 index 000000000..3917b6e62 --- /dev/null +++ b/src/main/java/wooteco/subway/dao/line/InmemoryLineDao.java @@ -0,0 +1,78 @@ +package wooteco.subway.dao.line; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.util.ReflectionUtils; +import wooteco.subway.domain.Line; + +public class InmemoryLineDao implements LineDao { + + private static InmemoryLineDao INSTANCE; + private final Map lines = new HashMap<>(); + private Long seq = 0L; + + private InmemoryLineDao() { + } + + public static synchronized InmemoryLineDao getInstance() { + if (INSTANCE == null) { + INSTANCE = new InmemoryLineDao(); + } + return INSTANCE; + } + + public void clear() { + lines.clear(); + } + + @Override + public long save(final Line line) { + Line persistLine = createNewObject(line); + lines.put(persistLine.getId(), persistLine); + return persistLine.getId(); + } + + private Line createNewObject(Line line) { + Field field = ReflectionUtils.findField(Line.class, "id"); + field.setAccessible(true); + ReflectionUtils.setField(field, line, ++seq); + return line; + } + + @Override + public Line findById(final Long id) { + return lines.get(id); + } + + @Override + public List findAll() { + return new ArrayList<>(lines.values()); + } + + @Override + public boolean existByName(final String name) { + return lines.values() + .stream() + .anyMatch(line -> line.isSameName(name)); + } + + @Override + public boolean existById(final Long id) { + return lines.containsKey(id); + } + + @Override + public int update(final Line line) { + lines.put(line.getId(), line); + return 1; + } + + @Override + public int delete(final Long id) { + lines.remove(id); + return 1; + } +} diff --git a/src/main/java/wooteco/subway/dao/line/JdbcLineDao.java b/src/main/java/wooteco/subway/dao/line/JdbcLineDao.java new file mode 100644 index 000000000..cc13d3dd7 --- /dev/null +++ b/src/main/java/wooteco/subway/dao/line/JdbcLineDao.java @@ -0,0 +1,77 @@ +package wooteco.subway.dao.line; + +import java.sql.PreparedStatement; +import java.util.List; +import java.util.Objects; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import wooteco.subway.domain.Line; + +@Repository +public class JdbcLineDao implements LineDao { + + private final JdbcTemplate jdbcTemplate; + private final RowMapper lineRowMapper = (rs, rowNum) -> new Line( + rs.getLong("id"), + rs.getString("name"), + rs.getString("color") + ); + + public JdbcLineDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public long save(final Line line) { + final String sql = "insert into LINE (name, color) values(?, ?)"; + + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, line.getName()); + ps.setString(2, line.getColor()); + return ps; + }, keyHolder); + + return Objects.requireNonNull(keyHolder.getKey()).longValue(); + } + + @Override + public Line findById(final Long id) { + final String sql = "select * from LINE where id = ?"; + return jdbcTemplate.queryForObject(sql, lineRowMapper, id); + } + + @Override + public List findAll() { + final String sql = "select * from LINE"; + return jdbcTemplate.query(sql, lineRowMapper); + } + + @Override + public boolean existByName(final String name) { + final String sql = "select exists (select * from LINE where name = ?)"; + return jdbcTemplate.queryForObject(sql, Boolean.class, name); + } + + @Override + public boolean existById(final Long id) { + final String sql = "select exists (select * from LINE where id = ?)"; + return jdbcTemplate.queryForObject(sql, Boolean.class, id); + } + + @Override + public int update(final Line line) { + final String sql = "update LINE set name = ?, color = ? where id = ?"; + return jdbcTemplate.update(sql, line.getName(), line.getColor(), line.getId()); + } + + @Override + public int delete(final Long id) { + final String sql = "delete from LINE where id = ?"; + return jdbcTemplate.update(sql, id); + } +} diff --git a/src/main/java/wooteco/subway/dao/line/LineDao.java b/src/main/java/wooteco/subway/dao/line/LineDao.java new file mode 100644 index 000000000..fc545412f --- /dev/null +++ b/src/main/java/wooteco/subway/dao/line/LineDao.java @@ -0,0 +1,21 @@ +package wooteco.subway.dao.line; + +import java.util.List; +import wooteco.subway.domain.Line; + +public interface LineDao { + + long save(Line line); + + Line findById(Long id); + + List findAll(); + + boolean existByName(String name); + + boolean existById(Long id); + + int update(Line line); + + int delete(Long id); +} diff --git a/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java b/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java new file mode 100644 index 000000000..36f85fed0 --- /dev/null +++ b/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java @@ -0,0 +1,66 @@ +package wooteco.subway.dao.section; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.util.ReflectionUtils; +import wooteco.subway.domain.Section; + +public class InmemorySectionDao implements SectionDao { + + private static InmemorySectionDao INSTANCE; + private final Map sections = new HashMap<>(); + private Long seq = 0L; + + private InmemorySectionDao() { + } + + public static synchronized InmemorySectionDao getInstance() { + if (INSTANCE == null) { + INSTANCE = new InmemorySectionDao(); + } + return INSTANCE; + } + + public void clear() { + sections.clear(); + } + + @Override + public long save(final Section section) { + Section persistSection = createNewObject(section); + sections.put(persistSection.getId(), persistSection); + return persistSection.getId(); + } + + private Section createNewObject(Section section) { + Field field = ReflectionUtils.findField(Section.class, "id"); + field.setAccessible(true); + ReflectionUtils.setField(field, section, ++seq); + return section; + } + + @Override + public List
findAllByLineId(final long lineId) { + return sections.values() + .stream() + .filter(section -> section.getLineId() == lineId) + .collect(Collectors.toList()); + } + + @Override + public int updateSections(final List
sections) { + clear(); + this.sections.putAll(sections.stream() + .collect(Collectors.toMap(Section::getId, section -> section))); + return sections.size(); + } + + @Override + public int delete(final long sectionId) { + sections.remove(sectionId); + return 1; + } +} diff --git a/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java b/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java new file mode 100644 index 000000000..32199c141 --- /dev/null +++ b/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java @@ -0,0 +1,86 @@ +package wooteco.subway.dao.section; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Station; + +@Repository +public class JdbcSectionDao implements SectionDao { + + private final JdbcTemplate jdbcTemplate; + private final RowMapper
sectionRowMapper = ((rs, rowNum) -> new Section( + rs.getLong("id"), + rs.getLong("line_id"), + new Station(rs.getLong("us_id"), rs.getString("us_name")), + new Station(rs.getLong("ds_id"), rs.getString("ds_name")), + rs.getInt("distance") + )); + + public JdbcSectionDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public long save(final Section section) { + final String sql = "insert into SECTION (line_id, up_station_id, down_station_id, distance) values(?, ?, ?, ?)"; + + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setLong(1, section.getLineId()); + ps.setLong(2, section.getUpStation().getId()); + ps.setLong(3, section.getDownStation().getId()); + ps.setInt(4, section.getDistance()); + return ps; + }, keyHolder); + + return Objects.requireNonNull(keyHolder.getKey()).longValue(); + } + + @Override + public List
findAllByLineId(final long lineId) { + final String sql = "select s.id as id, s.line_id as line_id, us.id as us_id, us.name as us_name," + + "ds.id as ds_id, ds.name as ds_name, s.distance as distance from SECTION as s " + + "join STATION as us on us.id = s.up_station_id " + + "join STATION as ds on ds.id = s.down_station_id " + + "where line_id = ?"; + + return jdbcTemplate.query(sql, sectionRowMapper, lineId); + } + + @Override + public int updateSections(final List
sections) { + final String sql = "update SECTION set up_station_id = ?, down_station_id = ?, distance = ? where id = ?"; + return jdbcTemplate.batchUpdate(sql, + new BatchPreparedStatementSetter() { + @Override + public void setValues(final PreparedStatement ps, final int i) throws SQLException { + Section section = sections.get(i); + ps.setLong(1, section.getUpStation().getId()); + ps.setLong(2, section.getDownStation().getId()); + ps.setInt(3, section.getDistance()); + ps.setLong(4, section.getId()); + } + + @Override + public int getBatchSize() { + return sections.size(); + } + }).length; + } + + @Override + public int delete(final long sectionId) { + final String sql = "delete from SECTION where id = ?"; + return jdbcTemplate.update(sql, sectionId); + } +} diff --git a/src/main/java/wooteco/subway/dao/section/SectionDao.java b/src/main/java/wooteco/subway/dao/section/SectionDao.java new file mode 100644 index 000000000..803ce9f54 --- /dev/null +++ b/src/main/java/wooteco/subway/dao/section/SectionDao.java @@ -0,0 +1,15 @@ +package wooteco.subway.dao.section; + +import java.util.List; +import wooteco.subway.domain.Section; + +public interface SectionDao { + + long save(Section section); + + List
findAllByLineId(long lineId); + + int updateSections(List
sections); + + int delete(long sectionId); +} diff --git a/src/main/java/wooteco/subway/dao/station/InmemoryStationDao.java b/src/main/java/wooteco/subway/dao/station/InmemoryStationDao.java new file mode 100644 index 000000000..49dbe8410 --- /dev/null +++ b/src/main/java/wooteco/subway/dao/station/InmemoryStationDao.java @@ -0,0 +1,72 @@ +package wooteco.subway.dao.station; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.util.ReflectionUtils; +import wooteco.subway.domain.Station; + +public class InmemoryStationDao implements StationDao { + + private static InmemoryStationDao INSTANCE; + private final Map stations = new HashMap<>(); + private Long seq = 0L; + + private InmemoryStationDao() { + } + + public static synchronized InmemoryStationDao getInstance() { + if (INSTANCE == null) { + INSTANCE = new InmemoryStationDao(); + } + return INSTANCE; + } + + public void clear() { + stations.clear(); + } + + @Override + public long save(Station station) { + Station persistStation = createNewObject(station); + stations.put(persistStation.getId(), persistStation); + return persistStation.getId(); + } + + private Station createNewObject(Station station) { + Field field = ReflectionUtils.findField(Station.class, "id"); + field.setAccessible(true); + ReflectionUtils.setField(field, station, ++seq); + return station; + } + + @Override + public List findAll() { + return new ArrayList<>(stations.values()); + } + + @Override + public Station findById(final Long id) { + return stations.get(id); + } + + @Override + public boolean existByName(final String name) { + return stations.values() + .stream() + .anyMatch(station -> station.isSameName(name)); + } + + @Override + public boolean existById(final Long id) { + return stations.containsKey(id); + } + + @Override + public int delete(final Long stationId) { + stations.remove(stationId); + return 1; + } +} diff --git a/src/main/java/wooteco/subway/dao/station/JdbcStationDao.java b/src/main/java/wooteco/subway/dao/station/JdbcStationDao.java new file mode 100644 index 000000000..af0d2204f --- /dev/null +++ b/src/main/java/wooteco/subway/dao/station/JdbcStationDao.java @@ -0,0 +1,69 @@ +package wooteco.subway.dao.station; + +import java.sql.PreparedStatement; +import java.util.List; +import java.util.Objects; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import wooteco.subway.domain.Station; + +@Repository +public class JdbcStationDao implements StationDao { + + private final JdbcTemplate jdbcTemplate; + private final RowMapper stationRowMapper = (rs, rowNum) -> new Station( + rs.getLong("id"), + rs.getString("name") + ); + + public JdbcStationDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public long save(final Station station) { + final String sql = "insert into STATION (name) values (?)"; + + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, station.getName()); + return ps; + }, keyHolder); + + return Objects.requireNonNull(keyHolder.getKey()).longValue(); + } + + @Override + public List findAll() { + final String sql = "select * from STATION"; + return jdbcTemplate.query(sql, stationRowMapper); + } + + @Override + public Station findById(final Long id) { + final String sql = "select * from STATION where id = ?"; + return jdbcTemplate.queryForObject(sql, stationRowMapper, id); + } + + @Override + public boolean existByName(final String name) { + final String sql = "select exists (select * from STATION where name = ?)"; + return jdbcTemplate.queryForObject(sql, Boolean.class, name); + } + + @Override + public boolean existById(final Long id) { + final String sql = "select exists (select * from STATION where id = ?)"; + return jdbcTemplate.queryForObject(sql, Boolean.class, id); + } + + @Override + public int delete(final Long stationId) { + final String sql = "delete from STATION where id = ?"; + return jdbcTemplate.update(sql, stationId); + } +} diff --git a/src/main/java/wooteco/subway/dao/station/StationDao.java b/src/main/java/wooteco/subway/dao/station/StationDao.java new file mode 100644 index 000000000..7e30c75d1 --- /dev/null +++ b/src/main/java/wooteco/subway/dao/station/StationDao.java @@ -0,0 +1,19 @@ +package wooteco.subway.dao.station; + +import java.util.List; +import wooteco.subway.domain.Station; + +public interface StationDao { + + long save(Station station); + + List findAll(); + + Station findById(Long id); + + boolean existByName(String name); + + boolean existById(Long id); + + int delete(Long stationId); +} diff --git a/src/main/java/wooteco/subway/domain/Line.java b/src/main/java/wooteco/subway/domain/Line.java new file mode 100644 index 000000000..a8e17bd2e --- /dev/null +++ b/src/main/java/wooteco/subway/domain/Line.java @@ -0,0 +1,53 @@ +package wooteco.subway.domain; + +import java.util.Objects; + +public class Line { + + private final Long id; + private final String name; + private final String color; + + public Line(final Long id, final String name, final String color) { + this.id = id; + this.name = name; + this.color = color; + } + + public Line(final String name, final String color) { + this(null, name, color); + } + + public boolean isSameName(final String name) { + return this.name.equals(name); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Line line = (Line) o; + return Objects.equals(id, line.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/wooteco/subway/domain/Section.java b/src/main/java/wooteco/subway/domain/Section.java new file mode 100644 index 000000000..c68c5f28b --- /dev/null +++ b/src/main/java/wooteco/subway/domain/Section.java @@ -0,0 +1,128 @@ +package wooteco.subway.domain; + +import java.util.Objects; + +public class Section { + + private final Long id; + private final long lineId; + private final Station upStation; + private final Station downStation; + private final int distance; + + public Section(final Long id, final long lineId, final Station upStation, final Station downStation, + final int distance) { + validatePositiveDistance(distance); + validateDuplicateStation(upStation, downStation); + this.id = id; + this.lineId = lineId; + this.upStation = upStation; + this.downStation = downStation; + this.distance = distance; + } + + private void validatePositiveDistance(final int distance) { + if (distance <= 0) { + throw new IllegalArgumentException("구간의 길이는 양수만 들어올 수 있습니다."); + } + } + + private void validateDuplicateStation(final Station upStation, final Station downStation) { + if (upStation.equals(downStation)) { + throw new IllegalArgumentException("upstation과 downstation은 중복될 수 없습니다."); + } + } + + public Section(final long lineId, final Station upStation, final Station downStation, final int distance) { + this(null, lineId, upStation, downStation, distance); + } + + public Section(final Long id, final Section section) { + this(id, section.lineId, section.upStation, section.downStation, section.distance); + } + + public boolean isUpperSection(final Section section) { + return this.upStation.equals(section.downStation); + } + + public boolean isLowerSection(final Section section) { + return this.downStation.equals(section.upStation); + } + + public boolean isConnectedSection(final Section section) { + return containsStation(section.upStation) || containsStation(section.downStation); + } + + public boolean containsStation(final Station station) { + return isUpStation(station) || isDownStation(station); + } + + public boolean isUpStation(final Station station) { + return upStation.equals(station); + } + + public boolean isDownStation(final Station station) { + return downStation.equals(station); + } + + public boolean equalsUpStation(final Section section) { + return upStation.equals(section.upStation); + } + + public boolean equalsDownStation(final Section section) { + return downStation.equals(section.downStation); + } + + public boolean isEqualsOrLargerDistance(final Section section) { + return this.distance <= section.distance; + } + + public Section createMiddleSectionByDownStationSection(final Section section) { + return new Section(id, lineId, section.downStation, this.downStation, this.distance - section.distance); + } + + public Section createMiddleSectionByUpStationSection(final Section section) { + return new Section(id, lineId, this.upStation, section.upStation, this.distance - section.distance); + } + + public Section createExtensionSection(final Section section) { + return new Section(id, lineId, this.upStation, section.downStation, this.distance + section.distance); + } + + public Long getId() { + return id; + } + + public long getLineId() { + return lineId; + } + + public Station getUpStation() { + return upStation; + } + + public Station getDownStation() { + return downStation; + } + + public int getDistance() { + return distance; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Section section = (Section) o; + return Objects.equals(id, section.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/wooteco/subway/domain/Sections.java b/src/main/java/wooteco/subway/domain/Sections.java new file mode 100644 index 000000000..96c088ecd --- /dev/null +++ b/src/main/java/wooteco/subway/domain/Sections.java @@ -0,0 +1,256 @@ +package wooteco.subway.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.function.Supplier; +import wooteco.subway.exception.NotFoundException; + +public class Sections { + + private static final int LIMIT_REMOVE_SIZE = 1; + + private final List
sections; + + public Sections(final List
sections) { + this.sections = new ArrayList<>(sections); + validateSectionsSize(); + } + + private void validateSectionsSize() { + if (this.sections.isEmpty()) { + throw new IllegalArgumentException("sections는 크기가 0으로는 생성할 수 없습니다."); + } + } + + public List calculateSortedStations() { + return createSortedStations(calculateTopSection()); + } + + private List createSortedStations(Section section) { + List stations = new ArrayList<>(); + stations.add(section.getUpStation()); + + while (hasLowerSection(section)) { + stations.add(section.getDownStation()); + section = lowerSection(section); + } + stations.add(section.getDownStation()); + return stations; + } + + public void addSection(final Section section) { + validateAdditionalSection(section); + if (isUpperThanTopSection(section) || isLowerThanBottomSection(section)) { + sections.add(section); + return; + } + if (hasEqualsUpStation(section)) { + addSectionByEqualsUpStation(section); + return; + } + addSectionByEqualsDownStation(section); + } + + private void validateAdditionalSection(final Section section) { + if (hasNotConnectedSection(section)) { + throw new IllegalStateException("구간 추가는 기존의 상행역 하행역 중 하나를 포함해야합니다."); + } + if (existUpStationToDownStation(section)) { + throw new IllegalStateException("이미 상행에서 하행으로 갈 수 있는 구간이 존재합니다."); + } + } + + private boolean hasNotConnectedSection(final Section section) { + return sections.stream() + .noneMatch(section::isConnectedSection); + } + + private boolean existUpStationToDownStation(final Section section) { + return hasEqualsUpStation(section) && hasEqualsDownStation(section); + } + + private boolean hasEqualsDownStation(final Section section) { + return sections.stream() + .anyMatch(section::equalsDownStation); + } + + private boolean isUpperThanTopSection(final Section section) { + return calculateTopSection().isUpperSection(section); + } + + private boolean isLowerThanBottomSection(final Section section) { + return calculateLastSection().isLowerSection(section); + } + + private void addSectionByEqualsUpStation(final Section section) { + Section updatedSection = findSection(section::equalsUpStation); + validateEqualsOrLargerDistance(section, updatedSection); + sections.add(section); + sections.removeIf(updatedSection::equals); + sections.add(updatedSection.createMiddleSectionByDownStationSection(section)); + } + + private void addSectionByEqualsDownStation(final Section section) { + Section updatedSection = findSection(section::equalsDownStation); + validateEqualsOrLargerDistance(section, updatedSection); + sections.add(section); + sections.removeIf(updatedSection::equals); + sections.add(updatedSection.createMiddleSectionByUpStationSection(section)); + } + + public Section removeSection(final Station station) { + if (notContainsStation(station)) { + throw new IllegalStateException("해당 역은 구간에 포함되어있지 않습니다."); + } + if (sections.size() == LIMIT_REMOVE_SIZE) { + throw new IllegalStateException("구간이 하나뿐이어서 제거할 수 없습니다."); + } + if (isTopStation(station)) { + return removeTopSection(); + } + if (isBottomStation(station)) { + return removeBottomSection(); + } + return removeIntervalStation(station); + } + + private boolean notContainsStation(final Station station) { + return sections.stream() + .noneMatch(section -> section.containsStation(station)); + } + + private boolean isTopStation(final Station station) { + return calculateTopSection().isUpStation(station); + } + + private Section removeTopSection() { + Section topSection = calculateTopSection(); + sections.removeIf(topSection::equals); + return topSection; + } + + private boolean isBottomStation(final Station station) { + return calculateLastSection().isDownStation(station); + } + + private Section removeBottomSection() { + Section bottomSection = calculateLastSection(); + sections.removeIf(bottomSection::equals); + return bottomSection; + } + + private Section removeIntervalStation(final Station station) { + Section removeSection = findSection(isUpStation(station)); + Section updateSection = findSection(isDownStations(station)); + + sections.removeIf(removeSection::equals); + sections.removeIf(updateSection::equals); + sections.add(updateSection.createExtensionSection(removeSection)); + return removeSection; + } + + private Predicate
isUpStation(final Station station) { + return section -> section.isUpStation(station); + } + + private Predicate
isDownStations(final Station station) { + return section -> section.isDownStation(station); + } + + private Section calculateTopSection() { + return calculateTopSection(findAnySection()); + } + + private Section calculateTopSection(final Section section) { + if (!hasUpperSection(section)) { + return section; + } + return calculateTopSection(upperSection(section)); + } + + private boolean hasUpperSection(final Section section) { + return sections.stream() + .anyMatch(section::isUpperSection); + } + + private Section upperSection(final Section section) { + return sections.stream() + .filter(section::isUpperSection) + .findFirst() + .orElseThrow(notFoundSectionSupplier()); + } + + private Section calculateLastSection() { + return calculateLastSection(findAnySection()); + } + + private Section calculateLastSection(final Section section) { + if (!hasLowerSection(section)) { + return section; + } + return calculateLastSection(lowerSection(section)); + } + + private boolean hasLowerSection(final Section section) { + return sections.stream() + .anyMatch(section::isLowerSection); + } + + private Section lowerSection(final Section section) { + return sections.stream() + .filter(section::isLowerSection) + .findFirst() + .orElseThrow(notFoundSectionSupplier()); + } + + private Section findAnySection() { + return sections.stream() + .findAny() + .orElseThrow(notFoundSectionSupplier()); + } + + private boolean hasEqualsUpStation(final Section section) { + return sections.stream() + .anyMatch(section::equalsUpStation); + } + + private Section findSection(final Predicate
isDesiredSection) { + return sections.stream() + .filter(isDesiredSection) + .findFirst() + .orElseThrow(notFoundSectionSupplier()); + } + + private void validateEqualsOrLargerDistance(final Section section, final Section updatedSection) { + if (updatedSection.isEqualsOrLargerDistance(section)) { + throw new IllegalStateException("기존 길이보다 길거나 같은 구간은 중간에 추가될 수 없습니다."); + } + } + + private Supplier notFoundSectionSupplier() { + return () -> new NotFoundException("section을 찾을 수 없습니다."); + } + + public List
getSections() { + return List.copyOf(sections); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Sections sections1 = (Sections) o; + return Objects.equals(sections, sections1.sections); + } + + @Override + public int hashCode() { + return Objects.hash(sections); + } +} diff --git a/src/main/java/wooteco/subway/domain/Station.java b/src/main/java/wooteco/subway/domain/Station.java index 55c50db50..8e8a81703 100644 --- a/src/main/java/wooteco/subway/domain/Station.java +++ b/src/main/java/wooteco/subway/domain/Station.java @@ -1,11 +1,11 @@ package wooteco.subway.domain; +import java.util.Objects; + public class Station { - private Long id; - private String name; - public Station() { - } + private final Long id; + private final String name; public Station(Long id, String name) { this.id = id; @@ -13,7 +13,11 @@ public Station(Long id, String name) { } public Station(String name) { - this.name = name; + this(null, name); + } + + public boolean isSameName(final String name) { + return this.name.equals(name); } public Long getId() { @@ -23,4 +27,22 @@ public Long getId() { public String getName() { return name; } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Station station = (Station) o; + return Objects.equals(id, station.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } + diff --git a/src/main/java/wooteco/subway/dto/LineRequest.java b/src/main/java/wooteco/subway/dto/LineRequest.java deleted file mode 100644 index c44e7f393..000000000 --- a/src/main/java/wooteco/subway/dto/LineRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package wooteco.subway.dto; - -public class LineRequest { - private String name; - private String color; - private Long upStationId; - private Long downStationId; - private int distance; - - public LineRequest() { - } - - public LineRequest(String name, String color, Long upStationId, Long downStationId, int distance) { - this.name = name; - this.color = color; - this.upStationId = upStationId; - this.downStationId = downStationId; - this.distance = distance; - } - - public String getName() { - return name; - } - - public String getColor() { - return color; - } - - public Long getUpStationId() { - return upStationId; - } - - public Long getDownStationId() { - return downStationId; - } - - public int getDistance() { - return distance; - } -} diff --git a/src/main/java/wooteco/subway/dto/LineResponse.java b/src/main/java/wooteco/subway/dto/LineResponse.java deleted file mode 100644 index a10a7ee74..000000000 --- a/src/main/java/wooteco/subway/dto/LineResponse.java +++ /dev/null @@ -1,33 +0,0 @@ -package wooteco.subway.dto; - -import java.util.List; - -public class LineResponse { - private Long id; - private String name; - private String color; - private List stations; - - public LineResponse(Long id, String name, String color, List stations) { - this.id = id; - this.name = name; - this.color = color; - this.stations = stations; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getColor() { - return color; - } - - public List getStations() { - return stations; - } -} diff --git a/src/main/java/wooteco/subway/dto/SectionRequest.java b/src/main/java/wooteco/subway/dto/SectionRequest.java deleted file mode 100644 index 9c3532372..000000000 --- a/src/main/java/wooteco/subway/dto/SectionRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package wooteco.subway.dto; - -public class SectionRequest { - private Long upStationId; - private Long downStationId; - private int distance; - - public SectionRequest() { - } - - public SectionRequest(Long upStationId, Long downStationId, int distance) { - this.upStationId = upStationId; - this.downStationId = downStationId; - this.distance = distance; - } - - public Long getUpStationId() { - return upStationId; - } - - public Long getDownStationId() { - return downStationId; - } - - public int getDistance() { - return distance; - } -} diff --git a/src/main/java/wooteco/subway/dto/StationRequest.java b/src/main/java/wooteco/subway/dto/StationRequest.java deleted file mode 100644 index 8a575e137..000000000 --- a/src/main/java/wooteco/subway/dto/StationRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package wooteco.subway.dto; - -import wooteco.subway.domain.Station; - -public class StationRequest { - private String name; - - public StationRequest() { - } - - public StationRequest(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public Station toStation() { - return new Station(name); - } -} diff --git a/src/main/java/wooteco/subway/dto/SubwayErrorResponse.java b/src/main/java/wooteco/subway/dto/SubwayErrorResponse.java new file mode 100644 index 000000000..d62d4208e --- /dev/null +++ b/src/main/java/wooteco/subway/dto/SubwayErrorResponse.java @@ -0,0 +1,35 @@ +package wooteco.subway.dto; + +import java.util.stream.Collectors; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.web.bind.MethodArgumentNotValidException; + +public class SubwayErrorResponse { + + private static final String MESSAGE_JOINING_DELIMITER = ","; + + private String message; + + private SubwayErrorResponse() { + } + + private SubwayErrorResponse(final String message) { + this.message = message; + } + + public static SubwayErrorResponse from(RuntimeException exception) { + return new SubwayErrorResponse(exception.getMessage()); + } + + public static SubwayErrorResponse from(MethodArgumentNotValidException exception) { + return new SubwayErrorResponse(exception.getBindingResult() + .getFieldErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(MESSAGE_JOINING_DELIMITER))); + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/wooteco/subway/dto/line/LineResponse.java b/src/main/java/wooteco/subway/dto/line/LineResponse.java new file mode 100644 index 000000000..bab7eaf9a --- /dev/null +++ b/src/main/java/wooteco/subway/dto/line/LineResponse.java @@ -0,0 +1,48 @@ +package wooteco.subway.dto.line; + +import java.util.List; +import java.util.stream.Collectors; +import wooteco.subway.domain.Line; +import wooteco.subway.domain.Station; +import wooteco.subway.dto.station.StationResponse; + +public class LineResponse { + + private Long id; + private String name; + private String color; + private List stations; + + private LineResponse() { + } + + private LineResponse(Long id, String name, String color, List stations) { + this.id = id; + this.name = name; + this.color = color; + this.stations = stations; + } + + public static LineResponse of(Line line, List stations) { + return new LineResponse(line.getId(), line.getName(), line.getColor(), + stations.stream() + .map(StationResponse::from) + .collect(Collectors.toList())); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public List getStations() { + return stations; + } +} diff --git a/src/main/java/wooteco/subway/dto/line/LineSaveRequest.java b/src/main/java/wooteco/subway/dto/line/LineSaveRequest.java new file mode 100644 index 000000000..cfe337aab --- /dev/null +++ b/src/main/java/wooteco/subway/dto/line/LineSaveRequest.java @@ -0,0 +1,59 @@ +package wooteco.subway.dto.line; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Positive; +import wooteco.subway.domain.Line; + +public class LineSaveRequest { + + @NotBlank(message = "line 이름은 공백 혹은 null이 들어올 수 없습니다.") + private String name; + + @NotBlank(message = "line 색상은 공백 혹은 null이 들어올 수 없습니다.") + private String color; + + @Positive(message = "상행역의 id는 양수 값만 들어올 수 있습니다.") + private long upStationId; + + @Positive(message = "하행역의 id는 양수 값만 들어올 수 있습니다.") + private long downStationId; + + @Positive(message = "상행-하행 노선 길이는 양수 값만 들어올 수 있습니다.") + private int distance; + + private LineSaveRequest() { + } + + public LineSaveRequest(final String name, final String color, final long upStationId, final long downStationId, + final int distance) { + this.name = name; + this.color = color; + this.upStationId = upStationId; + this.downStationId = downStationId; + this.distance = distance; + } + + public Line toLine() { + return new Line(name, color); + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public long getUpStationId() { + return upStationId; + } + + public long getDownStationId() { + return downStationId; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/wooteco/subway/dto/line/LineUpdateRequest.java b/src/main/java/wooteco/subway/dto/line/LineUpdateRequest.java new file mode 100644 index 000000000..2c90899ce --- /dev/null +++ b/src/main/java/wooteco/subway/dto/line/LineUpdateRequest.java @@ -0,0 +1,33 @@ +package wooteco.subway.dto.line; + +import javax.validation.constraints.NotBlank; +import wooteco.subway.domain.Line; + +public class LineUpdateRequest { + + @NotBlank(message = "line 이름은 공백 혹은 null이 들어올 수 없습니다.") + private String name; + + @NotBlank(message = "line 색상은 공백 혹은 null이 들어올 수 없습니다.") + private String color; + + private LineUpdateRequest() { + } + + public LineUpdateRequest(final String name, final String color) { + this.name = name; + this.color = color; + } + + public Line toLineWithId(final Long lineId) { + return new Line(lineId, name, color); + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } +} diff --git a/src/main/java/wooteco/subway/dto/section/SectionSaveRequest.java b/src/main/java/wooteco/subway/dto/section/SectionSaveRequest.java new file mode 100644 index 000000000..f2782bc6e --- /dev/null +++ b/src/main/java/wooteco/subway/dto/section/SectionSaveRequest.java @@ -0,0 +1,36 @@ +package wooteco.subway.dto.section; + +import javax.validation.constraints.Positive; + +public class SectionSaveRequest { + + @Positive(message = "상행역의 id는 양수 값만 들어올 수 있습니다.") + private long upStationId; + + @Positive(message = "하행역의 id는 양수 값만 들어올 수 있습니다.") + private long downStationId; + + @Positive(message = "상행-하행 노선 길이는 양수 값만 들어올 수 있습니다.") + private int distance; + + private SectionSaveRequest() { + } + + public SectionSaveRequest(final long upStationId, final long downStationId, final int distance) { + this.upStationId = upStationId; + this.downStationId = downStationId; + this.distance = distance; + } + + public long getUpStationId() { + return upStationId; + } + + public long getDownStationId() { + return downStationId; + } + + public int getDistance() { + return distance; + } +} diff --git a/src/main/java/wooteco/subway/dto/StationResponse.java b/src/main/java/wooteco/subway/dto/station/StationResponse.java similarity index 53% rename from src/main/java/wooteco/subway/dto/StationResponse.java rename to src/main/java/wooteco/subway/dto/station/StationResponse.java index dd2153f92..ed54ad1da 100644 --- a/src/main/java/wooteco/subway/dto/StationResponse.java +++ b/src/main/java/wooteco/subway/dto/station/StationResponse.java @@ -1,22 +1,24 @@ -package wooteco.subway.dto; +package wooteco.subway.dto.station; import wooteco.subway.domain.Station; -import java.util.List; -import java.util.stream.Collectors; - public class StationResponse { + private Long id; private String name; - public StationResponse() { + private StationResponse() { } - public StationResponse(Long id, String name) { + private StationResponse(Long id, String name) { this.id = id; this.name = name; } + public static StationResponse from(Station station) { + return new StationResponse(station.getId(), station.getName()); + } + public Long getId() { return id; } diff --git a/src/main/java/wooteco/subway/dto/station/StationSaveRequest.java b/src/main/java/wooteco/subway/dto/station/StationSaveRequest.java new file mode 100644 index 000000000..6e91222fb --- /dev/null +++ b/src/main/java/wooteco/subway/dto/station/StationSaveRequest.java @@ -0,0 +1,25 @@ +package wooteco.subway.dto.station; + +import javax.validation.constraints.NotBlank; +import wooteco.subway.domain.Station; + +public class StationSaveRequest { + + @NotBlank(message = "station 이름은 공백 혹은 null이 들어올 수 없습니다.") + private String name; + + private StationSaveRequest() { + } + + public StationSaveRequest(final String name) { + this.name = name; + } + + public Station toStation() { + return new Station(name); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/wooteco/subway/exception/NotFoundException.java b/src/main/java/wooteco/subway/exception/NotFoundException.java new file mode 100644 index 000000000..55b7d1c5e --- /dev/null +++ b/src/main/java/wooteco/subway/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package wooteco.subway.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(final String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/service/LineService.java b/src/main/java/wooteco/subway/service/LineService.java new file mode 100644 index 000000000..442d12eaa --- /dev/null +++ b/src/main/java/wooteco/subway/service/LineService.java @@ -0,0 +1,82 @@ +package wooteco.subway.service; + +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.subway.dao.line.LineDao; +import wooteco.subway.dao.section.SectionDao; +import wooteco.subway.dao.station.StationDao; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Sections; +import wooteco.subway.domain.Station; +import wooteco.subway.dto.line.LineResponse; +import wooteco.subway.dto.line.LineSaveRequest; +import wooteco.subway.dto.line.LineUpdateRequest; +import wooteco.subway.exception.NotFoundException; + +@Service +@Transactional(readOnly = true) +public class LineService { + + private final LineDao lineDao; + private final StationDao stationDao; + private final SectionDao sectionDao; + + public LineService(final LineDao lineDao, final StationDao stationDao, final SectionDao sectionDao) { + this.lineDao = lineDao; + this.stationDao = stationDao; + this.sectionDao = sectionDao; + } + + @Transactional + public LineResponse save(final LineSaveRequest lineSaveRequest) { + if (lineDao.existByName(lineSaveRequest.getName())) { + throw new IllegalStateException("이미 존재하는 노선 이름입니다."); + } + long savedLineId = lineDao.save(lineSaveRequest.toLine()); + saveLineSection(savedLineId, lineSaveRequest); + return findById(savedLineId); + } + + private void saveLineSection(final long lineId, final LineSaveRequest lineSaveRequest) { + Station upStation = stationDao.findById(lineSaveRequest.getUpStationId()); + Station downStation = stationDao.findById(lineSaveRequest.getDownStationId()); + Section section = new Section(lineId, upStation, downStation, lineSaveRequest.getDistance()); + sectionDao.save(section); + } + + public List findAll() { + return lineDao.findAll() + .stream() + .map(line -> LineResponse.of(line, findStationsByLineId(line.getId()))) + .collect(Collectors.toList()); + } + + public LineResponse findById(final Long lineId) { + return LineResponse.of(lineDao.findById(lineId), findStationsByLineId(lineId)); + } + + private List findStationsByLineId(final long lineId) { + Sections sections = new Sections(sectionDao.findAllByLineId(lineId)); + return sections.calculateSortedStations(); + } + + @Transactional + public void update(final long lineId, final LineUpdateRequest request) { + checkExistLine(lineId); + lineDao.update(request.toLineWithId(lineId)); + } + + @Transactional + public void delete(final Long lineId) { + checkExistLine(lineId); + lineDao.delete(lineId); + } + + private void checkExistLine(final Long lineId) { + if (!lineDao.existById(lineId)) { + throw new NotFoundException("존재하지 않는 Line입니다."); + } + } +} diff --git a/src/main/java/wooteco/subway/service/SectionService.java b/src/main/java/wooteco/subway/service/SectionService.java new file mode 100644 index 000000000..1c3df986f --- /dev/null +++ b/src/main/java/wooteco/subway/service/SectionService.java @@ -0,0 +1,55 @@ +package wooteco.subway.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.subway.dao.line.LineDao; +import wooteco.subway.dao.section.SectionDao; +import wooteco.subway.dao.station.StationDao; +import wooteco.subway.domain.Line; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Sections; +import wooteco.subway.domain.Station; +import wooteco.subway.dto.section.SectionSaveRequest; + +@Service +@Transactional(readOnly = true) +public class SectionService { + + private final LineDao lineDao; + private final SectionDao sectionDao; + private final StationDao stationDao; + + public SectionService(final LineDao lineDao, final SectionDao sectionDao, final StationDao stationDao) { + this.lineDao = lineDao; + this.sectionDao = sectionDao; + this.stationDao = stationDao; + } + + @Transactional + public int save(final long lineId, final SectionSaveRequest sectionSaveRequest) { + Section addSection = getAdditionSection(lineId, sectionSaveRequest); + Sections sections = new Sections(sectionDao.findAllByLineId(lineId)); + long savedSectionId = sectionDao.save(addSection); + sections.addSection(new Section(savedSectionId, addSection)); + + return sectionDao.updateSections(sections.getSections()); + } + + private Section getAdditionSection(final long lineId, final SectionSaveRequest sectionSaveRequest) { + Line line = lineDao.findById(lineId); + Station upStation = stationDao.findById(sectionSaveRequest.getUpStationId()); + Station downStation = stationDao.findById(sectionSaveRequest.getDownStationId()); + return new Section(line.getId(), upStation, downStation, sectionSaveRequest.getDistance()); + } + + @Transactional + public void delete(final long lineId, final long stationId) { + Station station = stationDao.findById(stationId); + Line line = lineDao.findById(lineId); + Sections sections = new Sections(sectionDao.findAllByLineId(line.getId())); + + Section section = sections.removeSection(station); + sectionDao.delete(section.getId()); + sectionDao.updateSections(sections.getSections()); + } +} diff --git a/src/main/java/wooteco/subway/service/StationService.java b/src/main/java/wooteco/subway/service/StationService.java new file mode 100644 index 000000000..159c39a60 --- /dev/null +++ b/src/main/java/wooteco/subway/service/StationService.java @@ -0,0 +1,53 @@ +package wooteco.subway.service; + +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.subway.dao.station.StationDao; +import wooteco.subway.domain.Station; +import wooteco.subway.dto.station.StationResponse; +import wooteco.subway.exception.NotFoundException; + +@Service +@Transactional(readOnly = true) +public class StationService { + + private final StationDao stationDao; + + public StationService(final StationDao stationDao) { + this.stationDao = stationDao; + } + + @Transactional + public StationResponse save(final Station station) { + if (stationDao.existByName(station.getName())) { + throw new IllegalStateException("이미 존재하는 역 이름입니다."); + } + long savedStationId = stationDao.save(station); + return StationResponse.from(findById(savedStationId)); + } + + private Station findById(final long savedStationId) { + return stationDao.findById(savedStationId); + } + + public List findAll() { + return stationDao.findAll() + .stream() + .map(StationResponse::from) + .collect(Collectors.toList()); + } + + @Transactional + public void delete(final Long stationId) { + validateExistStation(stationId); + stationDao.delete(stationId); + } + + private void validateExistStation(final Long stationId) { + if (!stationDao.existById(stationId)) { + throw new NotFoundException("존재하지 않는 Station입니다."); + } + } +} diff --git a/src/main/java/wooteco/subway/ui/StationController.java b/src/main/java/wooteco/subway/ui/StationController.java deleted file mode 100644 index 72481898a..000000000 --- a/src/main/java/wooteco/subway/ui/StationController.java +++ /dev/null @@ -1,38 +0,0 @@ -package wooteco.subway.ui; - -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import wooteco.subway.dao.StationDao; -import wooteco.subway.domain.Station; -import wooteco.subway.dto.StationRequest; -import wooteco.subway.dto.StationResponse; - -import java.net.URI; -import java.util.List; -import java.util.stream.Collectors; - -@RestController -public class StationController { - @PostMapping("/stations") - public ResponseEntity createStation(@RequestBody StationRequest stationRequest) { - Station station = new Station(stationRequest.getName()); - Station newStation = StationDao.save(station); - StationResponse stationResponse = new StationResponse(newStation.getId(), newStation.getName()); - return ResponseEntity.created(URI.create("/stations/" + newStation.getId())).body(stationResponse); - } - - @GetMapping(value = "/stations", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> showStations() { - List stations = StationDao.findAll(); - List stationResponses = stations.stream() - .map(it -> new StationResponse(it.getId(), it.getName())) - .collect(Collectors.toList()); - return ResponseEntity.ok().body(stationResponses); - } - - @DeleteMapping("/stations/{id}") - public ResponseEntity deleteStation(@PathVariable Long id) { - return ResponseEntity.noContent().build(); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..6e1573e62 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,9 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:maindb + username: sa + password: + h2: + console: + enabled: true diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..d44fdc202 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,28 @@ +DROP TABLE IF EXISTS STATION; +DROP TABLE IF EXISTS LINE; +DROP TABLE IF EXISTS SECTION; + +CREATE TABLE STATION +( + id bigint auto_increment not null, + name varchar(255) not null unique, + primary key (id) +); + +CREATE TABLE LINE +( + id bigint auto_increment not null, + name varchar(255) not null unique, + color varchar(20) not null, + primary key (id) +); + +create table SECTION +( + 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, + primary key (id) +); diff --git a/src/test/java/wooteco/subway/acceptance/AcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/AcceptanceTest.java index 8891ee415..5a85e17f2 100644 --- a/src/test/java/wooteco/subway/acceptance/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/AcceptanceTest.java @@ -4,12 +4,11 @@ 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; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -@ActiveProfiles("test") +@Sql(scripts = {"classpath:schema-truncate.sql"}, executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) public class AcceptanceTest { @LocalServerPort int port; diff --git a/src/test/java/wooteco/subway/acceptance/LineAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/LineAcceptanceTest.java new file mode 100644 index 000000000..43740015a --- /dev/null +++ b/src/test/java/wooteco/subway/acceptance/LineAcceptanceTest.java @@ -0,0 +1,190 @@ +package wooteco.subway.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static wooteco.subway.acceptance.StationAcceptanceTest.postStations; + +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 wooteco.subway.dto.line.LineResponse; +import wooteco.subway.dto.line.LineSaveRequest; +import wooteco.subway.dto.line.LineUpdateRequest; +import wooteco.subway.dto.station.StationResponse; +import wooteco.subway.dto.station.StationSaveRequest; + +class LineAcceptanceTest extends AcceptanceTest { + + public static ExtractableResponse postLines(final LineSaveRequest lineSaveRequest) { + return RestAssured.given().log().all() + .body(lineSaveRequest) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/lines") + .then().log().all() + .extract(); + } + + private ExtractableResponse getLines() { + return RestAssured.given().log().all() + .when() + .get("/lines") + .then().log().all() + .extract(); + } + + private ExtractableResponse getLine(final String location) { + return RestAssured.given().log().all() + .when() + .get(location) + .then().log().all() + .extract(); + } + + private ExtractableResponse putLine(final String location, final LineUpdateRequest lineUpdateRequest) { + return RestAssured.given().log().all() + .body(lineUpdateRequest) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .put(location) + .then().log().all() + .extract(); + } + + private ExtractableResponse deleteLine(final String location) { + return RestAssured.given().log().all() + .when() + .delete(location) + .then().log().all() + .extract(); + } + + @Test + @DisplayName("노선을 추가한다.") + void save() { + //given + Long upStationId = postStations(new StationSaveRequest("강남역")).response() + .as(StationResponse.class) + .getId(); + Long downStationId = postStations(new StationSaveRequest("역삼역")).response() + .as(StationResponse.class) + .getId(); + LineSaveRequest lineSaveRequest = new LineSaveRequest("신분당선", "bg-red-600", upStationId, downStationId, 3); + + //when + ExtractableResponse response = postLines(lineSaveRequest); + + //then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()), + () -> assertThat(response.header("Location")).isNotBlank() + ); + } + + @Test + @DisplayName("기존에 존재하는 노선의 이름으로 노선을 생성하면 노선을 생성할 수 없다.") + void createLinesWithExistNames() { + // given + Long upStationId = postStations(new StationSaveRequest("강남역")).response() + .as(StationResponse.class) + .getId(); + Long downStationId = postStations(new StationSaveRequest("역삼역")).response() + .as(StationResponse.class) + .getId(); + LineSaveRequest lineSaveRequest = new LineSaveRequest("신분당선", "bg-red-600", upStationId, downStationId, 3); + postLines(lineSaveRequest); + + // when + ExtractableResponse response = postLines(lineSaveRequest); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @Test + @DisplayName("전체 노선 목록을 조회한다.") + void findAllLines() { + //given + Long upStationId = postStations(new StationSaveRequest("강남역")).response() + .as(StationResponse.class) + .getId(); + Long downStationId = postStations(new StationSaveRequest("역삼역")).response() + .as(StationResponse.class) + .getId(); + postLines(new LineSaveRequest("신분당선", "bg-red-600", upStationId, downStationId, 3)); + postLines(new LineSaveRequest("분당선", "bg-green-600", upStationId, downStationId, 3)); + + // when + ExtractableResponse response = getLines(); + + //then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getList(".", LineResponse.class)).hasSize(2) + ); + } + + @Test + @DisplayName("단일 노선을 조회한다.") + void findLine() { + // given + Long upStationId = postStations(new StationSaveRequest("강남역")).response() + .as(StationResponse.class) + .getId(); + Long downStationId = postStations(new StationSaveRequest("역삼역")).response() + .as(StationResponse.class) + .getId(); + String location = postLines(new LineSaveRequest("신분당선", "bg-red-600", upStationId, downStationId, 3)) + .header("Location"); + + // when + ExtractableResponse response = getLine(location); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + @DisplayName("노선을 수정한다.") + void updateLine() { + //given + Long upStationId = postStations(new StationSaveRequest("강남역")).response() + .as(StationResponse.class) + .getId(); + Long downStationId = postStations(new StationSaveRequest("역삼역")).response() + .as(StationResponse.class) + .getId(); + String location = postLines(new LineSaveRequest("신분당선", "bg-red-600", upStationId, downStationId, 3)) + .header("Location"); + + // when + ExtractableResponse response = putLine(location, new LineUpdateRequest("분당선", "bg-green-600")); + + //then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + @DisplayName("노선을 삭제한다.") + void deleteLine() { + //given + Long upStationId = postStations(new StationSaveRequest("강남역")).response() + .as(StationResponse.class) + .getId(); + Long downStationId = postStations(new StationSaveRequest("역삼역")).response() + .as(StationResponse.class) + .getId(); + String location = postLines(new LineSaveRequest("신분당선", "bg-red-600", upStationId, downStationId, 3)) + .header("Location"); + + // when + ExtractableResponse response = deleteLine(location); + + //then + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } +} diff --git a/src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java new file mode 100644 index 000000000..cfef23a97 --- /dev/null +++ b/src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java @@ -0,0 +1,92 @@ +package wooteco.subway.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.subway.acceptance.LineAcceptanceTest.postLines; +import static wooteco.subway.acceptance.StationAcceptanceTest.postStations; + +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 wooteco.subway.dto.line.LineResponse; +import wooteco.subway.dto.line.LineSaveRequest; +import wooteco.subway.dto.section.SectionSaveRequest; +import wooteco.subway.dto.station.StationResponse; +import wooteco.subway.dto.station.StationSaveRequest; + +public class SectionAcceptanceTest extends AcceptanceTest { + + private ExtractableResponse postSections(final Long lineId, final SectionSaveRequest sectionSaveRequest) { + return RestAssured.given().log().all() + .body(sectionSaveRequest) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/lines/" + lineId + "/sections") + .then().log().all() + .extract(); + } + + private ExtractableResponse deleteSection(final Long lineId, final Long stationId) { + return RestAssured.given().log().all() + .param("stationId", stationId) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .delete("/lines/" + lineId + "/sections") + .then().log().all() + .extract(); + } + + @Test + @DisplayName("구간을 추가한다.") + void saveSection() { + // given + Long stationId1 = postStations(new StationSaveRequest("강남역")) + .as(StationResponse.class) + .getId(); + Long stationId2 = postStations(new StationSaveRequest("역삼역")) + .as(StationResponse.class) + .getId(); + Long stationId3 = postStations(new StationSaveRequest("선릉역")) + .as(StationResponse.class) + .getId(); + Long lineId = postLines(new LineSaveRequest("신분당선", "bg-red-600", stationId1, stationId3, 10)) + .as(LineResponse.class) + .getId(); + SectionSaveRequest sectionSaveRequest = new SectionSaveRequest(stationId1, stationId2, 3); + + // when + ExtractableResponse response = postSections(lineId, sectionSaveRequest); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + @DisplayName("구간을 삭제한다.") + void removeSection() { + // given + Long stationId1 = postStations(new StationSaveRequest("강남역")) + .as(StationResponse.class) + .getId(); + Long stationId2 = postStations(new StationSaveRequest("역삼역")) + .as(StationResponse.class) + .getId(); + Long stationId3 = postStations(new StationSaveRequest("선릉역")) + .as(StationResponse.class) + .getId(); + + Long lineId = postLines(new LineSaveRequest("신분당선", "bg-red-600", stationId1, stationId2, 10)) + .as(LineResponse.class) + .getId(); + postSections(lineId, new SectionSaveRequest(stationId2, stationId3, 10)); + + // when + ExtractableResponse response = deleteSection(lineId, stationId2); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } +} diff --git a/src/test/java/wooteco/subway/acceptance/StationAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/StationAcceptanceTest.java index f1b6f03aa..374c22fb8 100644 --- a/src/test/java/wooteco/subway/acceptance/StationAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/StationAcceptanceTest.java @@ -1,135 +1,111 @@ package wooteco.subway.acceptance; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import wooteco.subway.dto.StationRequest; -import wooteco.subway.dto.StationResponse; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; +import wooteco.subway.dto.station.StationResponse; +import wooteco.subway.dto.station.StationSaveRequest; @DisplayName("지하철역 관련 기능") public class StationAcceptanceTest extends AcceptanceTest { - private static final String 강남역 = "강남역"; - private static final String 역삼역 = "역삼역"; + + public static ExtractableResponse postStations(final StationSaveRequest stationSaveRequest) { + return RestAssured.given().log().all() + .body(stationSaveRequest) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/stations") + .then().log().all() + .extract(); + } + + private ExtractableResponse getAllStations() { + return RestAssured.given().log().all() + .when() + .get("/stations") + .then().log().all() + .extract(); + } + + private ExtractableResponse deleteStation(final String location) { + return RestAssured.given().log().all() + .when() + .delete(location) + .then().log().all() + .extract(); + } @DisplayName("지하철역을 생성한다.") @Test void createStation() { + // given + StationSaveRequest stationSaveRequest = new StationSaveRequest("강남역"); + // when - ExtractableResponse response = 지하철역_생성_요청(강남역); + ExtractableResponse response = postStations(stationSaveRequest); // then - 지하철역_생성됨(response); + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()), + () -> assertThat(response.header("Location")).isNotBlank() + ); } - @DisplayName("기존에 존재하는 지하철역 이름으로 지하철역을 생성한다.") + @DisplayName("기존에 존재하는 지하철역 이름으로 지하철역을 생성할 수 없다.") @Test void createStationWithDuplicateName() { - //given - 지하철역_등록되어_있음(강남역); + // given + postStations(new StationSaveRequest("강남역")); // when - ExtractableResponse response = 지하철역_생성_요청(강남역); + ExtractableResponse response = postStations(new StationSaveRequest("강남역")); // then - 지하철역_생성_실패됨(response); + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @DisplayName("지하철역을 조회한다.") @Test void getStations() { - // given - StationResponse stationResponse1 = 지하철역_등록되어_있음(강남역); - StationResponse stationResponse2 = 지하철역_등록되어_있음(역삼역); + /// given + ExtractableResponse createResponse1 = postStations(new StationSaveRequest("강남역")); + ExtractableResponse createResponse2 = postStations(new StationSaveRequest("역삼역")); // when - ExtractableResponse response = 지하철역_목록_조회_요청(); + ExtractableResponse response = getAllStations(); // then - 지하철역_목록_응답됨(response); - 지하철역_목록_포함됨(response, Arrays.asList(stationResponse1, stationResponse2)); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + List expectedLineIds = Arrays.asList(createResponse1, createResponse2).stream() + .map(it -> Long.parseLong(it.header("Location").split("/")[2])) + .collect(Collectors.toList()); + List resultLineIds = response.jsonPath().getList(".", StationResponse.class).stream() + .map(it -> it.getId()) + .collect(Collectors.toList()); + assertThat(resultLineIds).containsAll(expectedLineIds); } @DisplayName("지하철역을 제거한다.") @Test void deleteStation() { // given - StationResponse stationResponse = 지하철역_등록되어_있음(강남역); + String location = postStations(new StationSaveRequest("강남역")) + .header("Location"); // when - ExtractableResponse response = 지하철역_제거_요청(stationResponse); + ExtractableResponse response = deleteStation(location); // then - 지하철역_삭제됨(response); - } - - public static StationResponse 지하철역_등록되어_있음(String name) { - return 지하철역_생성_요청(name).as(StationResponse.class); - } - - public static ExtractableResponse 지하철역_생성_요청(String name) { - StationRequest stationRequest = new StationRequest(name); - - return RestAssured - .given().log().all() - .body(stationRequest) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when().post("/stations") - .then().log().all() - .extract(); - } - - public static ExtractableResponse 지하철역_목록_조회_요청() { - return RestAssured - .given().log().all() - .when().get("/stations") - .then().log().all() - .extract(); - } - - public static ExtractableResponse 지하철역_제거_요청(StationResponse stationResponse) { - return RestAssured - .given().log().all() - .when().delete("/stations/" + stationResponse.getId()) - .then().log().all() - .extract(); - } - - public static void 지하철역_생성됨(ExtractableResponse response) { - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.header("Location")).isNotBlank(); - } - - public static void 지하철역_생성_실패됨(ExtractableResponse response) { - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - public static void 지하철역_목록_응답됨(ExtractableResponse response) { - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - } - - public static void 지하철역_삭제됨(ExtractableResponse response) { assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); } - - public static void 지하철역_목록_포함됨(ExtractableResponse response, List createdResponses) { - List expectedLineIds = createdResponses.stream() - .map(it -> it.getId()) - .collect(Collectors.toList()); - - List resultLineIds = response.jsonPath().getList(".", StationResponse.class).stream() - .map(StationResponse::getId) - .collect(Collectors.toList()); - - assertThat(resultLineIds).containsAll(expectedLineIds); - } } diff --git a/src/test/java/wooteco/subway/controller/SubwayControllerAdviceTest.java b/src/test/java/wooteco/subway/controller/SubwayControllerAdviceTest.java new file mode 100644 index 000000000..82b82fcdd --- /dev/null +++ b/src/test/java/wooteco/subway/controller/SubwayControllerAdviceTest.java @@ -0,0 +1,43 @@ +package wooteco.subway.controller; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import wooteco.subway.acceptance.AcceptanceTest; +import wooteco.subway.dto.line.LineSaveRequest; + +class SubwayControllerAdviceTest extends AcceptanceTest { + + @Test + @DisplayName("없는 경로 api 요청") + void notFoundUrl() { + RestAssured.given().log().all() + .when() + .post("/test/not_found") + .then().log().all() + .statusCode(HttpStatus.NOT_FOUND.value()); + } + + @Test + @DisplayName("name이 null이고 distance가 음수인 line save request dto 요청할 경우 예외가 발생한다.") + void invalidNullNameLineSaveRequest() { + LineSaveRequest request = new LineSaveRequest(null, "bg-red-600", 1, 2, -1); + String nameNullErrorMessage = "line 이름은 공백 혹은 null이 들어올 수 없습니다."; + String distanceNegativeErrorMessage = "상행-하행 노선 길이는 양수 값만 들어올 수 있습니다."; + + RestAssured.given().log().all() + .body(request) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/lines") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .body("message", + allOf(containsString(nameNullErrorMessage), containsString(distanceNegativeErrorMessage))); + } +} diff --git a/src/test/java/wooteco/subway/dao/line/InmemoryLineDaoTest.java b/src/test/java/wooteco/subway/dao/line/InmemoryLineDaoTest.java new file mode 100644 index 000000000..3a84706cc --- /dev/null +++ b/src/test/java/wooteco/subway/dao/line/InmemoryLineDaoTest.java @@ -0,0 +1,78 @@ +package wooteco.subway.dao.line; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.domain.Line; + +class InmemoryLineDaoTest { + + private final InmemoryLineDao inmemoryLineDao = InmemoryLineDao.getInstance(); + + @AfterEach + void afterEach() { + inmemoryLineDao.clear(); + } + + @Test + @DisplayName("Line을 등록할 수 있다.") + void save() { + Line line = new Line("신분당선", "bg-red-600"); + + assertThat(inmemoryLineDao.save(line)).isNotNull(); + } + + @Test + @DisplayName("Line을 id로 조회할 수 있다.") + void findById() { + long id = inmemoryLineDao.save(new Line("신분당선", "bg-red-600")); + Line findLine = inmemoryLineDao.findById(id); + + assertThat(findLine.getId()).isEqualTo(id); + } + + @Test + @DisplayName("Line 전체 조회할 수 있다.") + void findAll() { + inmemoryLineDao.save(new Line("신분당선", "bg-red-600")); + inmemoryLineDao.save(new Line("분당선", "bg-green-600")); + + assertThat(inmemoryLineDao.findAll()).hasSize(2); + } + + @Test + @DisplayName("Line 이름이 존재하는지 확인할 수 있다.") + void existByName() { + inmemoryLineDao.save(new Line("신분당선", "bg-red-600")); + + assertThat(inmemoryLineDao.existByName("신분당선")).isTrue(); + } + + @Test + @DisplayName("id에 해당하는 Line이 존재하는지 확인할 수 있다.") + void existById() { + long id = inmemoryLineDao.save(new Line("신분당선", "bg-red-600")); + + assertThat(inmemoryLineDao.existById(id)).isNotNull(); + } + + @Test + @DisplayName("Line을 수정할 수 있다.") + void update() { + long id = inmemoryLineDao.save(new Line("신분당선", "bg-red-600")); + int result = inmemoryLineDao.update(new Line(id, "분당선", "bg-red-600")); + + assertThat(result).isEqualTo(1); + } + + @Test + @DisplayName("Line을 삭제할 수 있다.") + void delete() { + long id = inmemoryLineDao.save(new Line("신분당선", "bg-red-600")); + int result = inmemoryLineDao.delete(id); + + assertThat(result).isEqualTo(1); + } +} diff --git a/src/test/java/wooteco/subway/dao/line/JdbcLineDaoTest.java b/src/test/java/wooteco/subway/dao/line/JdbcLineDaoTest.java new file mode 100644 index 000000000..70e32e670 --- /dev/null +++ b/src/test/java/wooteco/subway/dao/line/JdbcLineDaoTest.java @@ -0,0 +1,85 @@ +package wooteco.subway.dao.line; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import wooteco.subway.domain.Line; + +@JdbcTest +class JdbcLineDaoTest { + + private LineDao lineDao; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + lineDao = new JdbcLineDao(jdbcTemplate); + } + + @Test + @DisplayName("Line을 등록할 수 있다.") + void save() { + Line line = new Line("신분당선", "bg-red-600"); + + assertThat(lineDao.save(line)).isNotNull(); + } + + @Test + @DisplayName("Line을 id로 조회할 수 있다.") + void findById() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + Line findLine = lineDao.findById(id); + + assertThat(findLine.getId()).isEqualTo(id); + } + + @Test + @DisplayName("Line 전체 조회할 수 있다.") + void findAll() { + lineDao.save(new Line("신분당선", "bg-red-600")); + lineDao.save(new Line("분당선", "bg-green-600")); + + assertThat(lineDao.findAll()).hasSize(2); + } + + @Test + @DisplayName("Line 이름이 존재하는지 확인할 수 있다.") + void existByName() { + lineDao.save(new Line("신분당선", "bg-red-600")); + + assertThat(lineDao.existByName("신분당선")).isTrue(); + } + + @Test + @DisplayName("id에 해당하는 Line이 존재하는지 확인할 수 있다.") + void existById() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + + assertThat(lineDao.existById(id)).isNotNull(); + } + + @Test + @DisplayName("Line을 수정할 수 있다.") + void update() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + int result = lineDao.update(new Line(id, "분당선", "bg-red-600")); + + assertThat(result).isEqualTo(1); + } + + @Test + @DisplayName("Line을 삭제할 수 있다.") + void delete() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + int result = lineDao.delete(id); + + assertThat(result).isEqualTo(1); + } +} diff --git a/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java b/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java new file mode 100644 index 000000000..69675602f --- /dev/null +++ b/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java @@ -0,0 +1,66 @@ +package wooteco.subway.dao.section; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.dao.line.InmemoryLineDao; +import wooteco.subway.dao.station.InmemoryStationDao; +import wooteco.subway.domain.Line; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Station; + +class InmemorySectionDaoTest { + + private final InmemorySectionDao sectionDao = InmemorySectionDao.getInstance(); + private final InmemoryStationDao stationDao = InmemoryStationDao.getInstance(); + private final InmemoryLineDao lineDao = InmemoryLineDao.getInstance(); + + @AfterEach + void afterEach() { + sectionDao.clear(); + stationDao.clear(); + lineDao.clear(); + } + + @Test + @DisplayName("Section 을 저장할 수 있다.") + void save() { + // given + Station upStation = new Station(1L, "오리"); + Station downStation = new Station(2L, "배카라"); + Section section = new Section(null, 1L, upStation, downStation, 1); + + // when + long savedSectionId = sectionDao.save(section); + + // then + assertThat(savedSectionId).isNotNull(); + } + + @Test + @DisplayName("Line Id에 해당하는 Section을 조회할 수 있다.") + void findAllByLineId() { + long lineId = 1L; + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + sectionDao.save(new Section(lineId, station1, station2, 2)); + sectionDao.save(new Section(lineId, station2, station3, 2)); + + assertThat(sectionDao.findAllByLineId(lineId)).hasSize(2); + } + + @Test + @DisplayName("Section을 삭제할 수 있다.") + void delete() { + long lineId = lineDao.save(new Line("신분당선", "bg-red-600")); + long stationId1 = stationDao.save(new Station("오리")); + long stationId2 = stationDao.save(new Station("배카라")); + long sectionId = sectionDao + .save(new Section(lineId, stationDao.findById(stationId1), stationDao.findById(stationId2), 10)); + + assertThat(sectionDao.delete(sectionId)).isEqualTo(1); + } +} diff --git a/src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java b/src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java new file mode 100644 index 000000000..6228b258a --- /dev/null +++ b/src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java @@ -0,0 +1,94 @@ +package wooteco.subway.dao.section; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import wooteco.subway.dao.line.JdbcLineDao; +import wooteco.subway.dao.line.LineDao; +import wooteco.subway.dao.station.JdbcStationDao; +import wooteco.subway.dao.station.StationDao; +import wooteco.subway.domain.Line; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Station; + +@JdbcTest +class JdbcSectionDaoTest { + + private SectionDao sectionDao; + private StationDao stationDao; + private LineDao lineDao; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + sectionDao = new JdbcSectionDao(jdbcTemplate); + stationDao = new JdbcStationDao(jdbcTemplate); + lineDao = new JdbcLineDao(jdbcTemplate); + } + + @Test + @DisplayName("Section 을 저장할 수 있다.") + void save() { + // given + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + Station upStation = stationDao.findById(stationDao.save(new Station("오리"))); + Station downStation = stationDao.findById(stationDao.save(new Station("배카라"))); + Section section = new Section(null, id, upStation, downStation, 1); + + // when + long savedSectionId = sectionDao.save(section); + + // then + assertThat(savedSectionId).isNotNull(); + } + + @Test + @DisplayName("Line Id에 해당하는 Section을 조회할 수 있다.") + void findAllByLineId() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + Station station3 = stationDao.findById(stationDao.save(new Station("오카라"))); + + sectionDao.save(new Section(id, station1, station2, 2)); + sectionDao.save(new Section(id, station2, station3, 2)); + + assertThat(sectionDao.findAllByLineId(id)).hasSize(2); + } + + @Test + @DisplayName("Sections를 업데이트할 수 있다.") + void updateSections() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + Station station3 = stationDao.findById(stationDao.save(new Station("오카라"))); + + long sectionId1 = sectionDao.save(new Section(id, station1, station2, 1)); + long sectionId2 = sectionDao.save(new Section(id, station2, station3, 1)); + + List
sections = List.of(new Section(sectionId1, id, station1, station3, 3), + new Section(sectionId2, id, station2, station3, 1)); + + assertThat(sectionDao.updateSections(sections)).isEqualTo(2); + } + + @Test + @DisplayName("Section을 삭제할 수 있다.") + void delete() { + long lineId = lineDao.save(new Line("신분당선", "bg-red-600")); + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + long sectionId = sectionDao.save(new Section(lineId, station1, station2, 10)); + + assertThat(sectionDao.delete(sectionId)).isEqualTo(1); + } +} diff --git a/src/test/java/wooteco/subway/dao/station/InmemoryStationDaoTest.java b/src/test/java/wooteco/subway/dao/station/InmemoryStationDaoTest.java new file mode 100644 index 000000000..2d231c879 --- /dev/null +++ b/src/test/java/wooteco/subway/dao/station/InmemoryStationDaoTest.java @@ -0,0 +1,69 @@ +package wooteco.subway.dao.station; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.domain.Station; + +class InmemoryStationDaoTest { + + private final InmemoryStationDao stationDao = InmemoryStationDao.getInstance(); + + @AfterEach + void afterEach() { + stationDao.clear(); + } + + @Test + @DisplayName("Station을 저장할 수 있다.") + void save() { + Station station = new Station("오리"); + long savedStationId = stationDao.save(station); + + assertThat(savedStationId).isNotNull(); + } + + @Test + @DisplayName("모든 Station을 조회할 수 있다.") + void findAll() { + stationDao.save(new Station("오리")); + stationDao.save(new Station("배카라")); + + assertThat(stationDao.findAll()).hasSize(2); + } + + @Test + @DisplayName("id로 Station을 조회한다.") + void findById() { + long savedStationId = stationDao.save(new Station("오리")); + + assertThat(stationDao.findById(savedStationId)).isNotNull(); + } + + @Test + @DisplayName("Station을 삭제할 수 있다.") + void delete() { + Long stationId = stationDao.save(new Station("오리")); + + assertThat(stationDao.delete(stationId)).isEqualTo(1); + } + + @Test + @DisplayName("Station 이름이 존재하는지 확인할 수 있다.") + void existByName() { + String name = "오리"; + stationDao.save(new Station(name)); + + assertThat(stationDao.existByName(name)).isTrue(); + } + + @Test + @DisplayName("id를 가진 Station이 존재하는지 확인할 수 있다.") + void existById() { + Long stationId = stationDao.save(new Station("오리")); + + assertThat(stationDao.existById(stationId)).isTrue(); + } +} diff --git a/src/test/java/wooteco/subway/dao/station/JdbcStationDaoTest.java b/src/test/java/wooteco/subway/dao/station/JdbcStationDaoTest.java new file mode 100644 index 000000000..44fa157bc --- /dev/null +++ b/src/test/java/wooteco/subway/dao/station/JdbcStationDaoTest.java @@ -0,0 +1,75 @@ +package wooteco.subway.dao.station; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import wooteco.subway.domain.Station; + +@JdbcTest +class JdbcStationDaoTest { + + private StationDao stationDao; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + stationDao = new JdbcStationDao(jdbcTemplate); + } + + @Test + @DisplayName("Station을 저장할 수 있다.") + void save() { + long savedStationId = stationDao.save(new Station("배카라")); + + assertThat(savedStationId).isNotNull(); + } + + @Test + @DisplayName("모든 Station을 조회할 수 있다.") + void findAll() { + stationDao.save(new Station("오리")); + stationDao.save(new Station("배카라")); + + assertThat(stationDao.findAll()).hasSize(2); + } + + @Test + @DisplayName("id로 Station을 조회한다.") + void findById() { + long savedStationId = stationDao.save(new Station("오리")); + + assertThat(stationDao.findById(savedStationId)).isNotNull(); + } + + @Test + @DisplayName("Station 이름이 존재하는지 확인할 수 있다.") + void existByName() { + String name = "배카라"; + stationDao.save(new Station(name)); + + assertThat(stationDao.existByName(name)).isTrue(); + } + + @Test + @DisplayName("id를 가진 Station이 존재하는지 확인할 수 있다.") + void existById() { + long savedStationId = stationDao.save(new Station("오리")); + + assertThat(stationDao.existById(savedStationId)).isTrue(); + } + + @Test + @DisplayName("Station을 삭제할 수 있다.") + void delete() { + long stationId = stationDao.save(new Station("오리")); + + assertThat(stationDao.delete(stationId)).isEqualTo(1); + } +} diff --git a/src/test/java/wooteco/subway/domain/SectionTest.java b/src/test/java/wooteco/subway/domain/SectionTest.java new file mode 100644 index 000000000..9ad3edbc7 --- /dev/null +++ b/src/test/java/wooteco/subway/domain/SectionTest.java @@ -0,0 +1,193 @@ +package wooteco.subway.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class SectionTest { + + @ParameterizedTest + @ValueSource(ints = {-1, 0}) + @DisplayName("생성 시 distance 가 0 이하인 경우 예외가 발생한다.") + void createExceptionByNotPositiveDistance(final int distance) { + Station upStation = new Station(1L, "오리"); + Station downStation = new Station(2L, "배카라"); + + assertThatThrownBy(() -> new Section(1L, upStation, downStation, distance)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("구간의 길이는 양수만 들어올 수 있습니다."); + } + + @Test + @DisplayName("upStation 과 downStation 이 중복될 경우 예외가 발생한다.") + void createExceptionDByDuplicateStationId() { + Station station = new Station(1L, "오리"); + + assertThatThrownBy(() -> new Section(1L, station, station, 1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("upstation과 downstation은 중복될 수 없습니다."); + } + + @Test + @DisplayName("입력된 section이 upSection인지 확인할 수 있다.") + void isUpSection() { + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + + Section upSection = new Section(1L, station1, station2, 2); + Section downSection = new Section(1L, station2, station3, 3); + + assertAll( + () -> assertThat(downSection.isUpperSection(upSection)).isTrue(), + () -> assertThat(upSection.isUpperSection(downSection)).isFalse() + ); + } + + @Test + @DisplayName("입력된 section이 downSection인지 확인할 수 있다.") + void isDownSection() { + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + + Section upSection = new Section(1L, station1, station2, 2); + Section downSection = new Section(1L, station2, station3, 3); + + assertAll( + () -> assertThat(upSection.isLowerSection(downSection)).isTrue(), + () -> assertThat(downSection.isLowerSection(upSection)).isFalse() + ); + } + + @Test + @DisplayName("입력된 section이 현재와 연결되어 상행 구간인지 하행 구간인지 확인할 수 있다.") + void isUpSectionOrDownSection() { + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + + Section section = new Section(1L, station1, station2, 2); + Section compareSection = new Section(1L, station1, station3, 3); + + assertThat(section.isConnectedSection(compareSection)).isTrue(); + } + + @Test + @DisplayName("입력된 station을 포함하는지 확인할 수 있다.") + void containsStation() { + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + + Section section = new Section(1L, station1, station2, 2); + + assertAll( + () -> assertThat(section.containsStation(station1)).isTrue(), + () -> assertThat(section.containsStation(station3)).isFalse() + ); + } + + @Test + @DisplayName("입력된 station이 upStation과 같은지 확인할 수 있다.") + void isUpStation() { + Station upStation = new Station(1L, "오리"); + Station downStation = new Station(2L, "배카라"); + Section section = new Section(1L, upStation, downStation, 2); + + assertAll( + () -> assertThat(section.isUpStation(upStation)).isTrue(), + () -> assertThat(section.isUpStation(downStation)).isFalse() + ); + } + + @Test + @DisplayName("입력된 station이 downStation과 같은지 확인할 수 있다.") + void isDownStation() { + Station upStation = new Station(1L, "오리"); + Station downStation = new Station(2L, "배카라"); + Section section = new Section(1L, upStation, downStation, 2); + + assertAll( + () -> assertThat(section.isDownStation(downStation)).isTrue(), + () -> assertThat(section.isDownStation(upStation)).isFalse() + ); + } + + @ParameterizedTest + @CsvSource(value = {"4,false", "5,true", "6,true"}) + @DisplayName("입력된 section의 길이가 크거나 같은 지 확인할 수 있다.") + void isEqualsOrLargerDistance(final int distance, final boolean expected) { + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + + Section section = new Section(1L, station1, station2, 5); + Section compareSection = new Section(1L, station1, station3, distance); + + assertThat(section.isEqualsOrLargerDistance(compareSection)).isEqualTo(expected); + } + + @Test + @DisplayName("가운데 있는 Section으로 하행 새로운 Section을 만들어 반환할 수 있다.") + void createMiddleSectionByDownStationSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section section = new Section(1L, 1L, station1, station3, 10); + Section middleSection = new Section(2L, 1L, station1, station2, 3); + + // when + Section updatedSection = section.createMiddleSectionByDownStationSection(middleSection); + + // then + assertThat(updatedSection) + .usingRecursiveComparison() + .isEqualTo(new Section(1L, 1L, station2, station3, 7)); + } + + @Test + @DisplayName("가운데 있는 Section으로 상행 새로운 Section을 만들어 반환할 수 있다.") + void createMiddleSectionByUpStationSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section section = new Section(1L, 1L, station1, station3, 10); + Section middleSection = new Section(2L, 1L, station2, station3, 3); + + // when + Section updatedSection = section.createMiddleSectionByUpStationSection(middleSection); + + // then + assertThat(updatedSection) + .usingRecursiveComparison() + .isEqualTo(new Section(1L, 1L, station1, station2, 7)); + } + + @Test + @DisplayName("연장된 Section을 만들어 반환할 수 있다.") + void createExtensionSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section section = new Section(1L, 1L, station1, station2, 10); + Section middleSection = new Section(2L, 1L, station2, station3, 3); + + // when + Section updatedSection = section.createExtensionSection(middleSection); + + // then + assertThat(updatedSection) + .usingRecursiveComparison() + .isEqualTo(new Section(1L, 1L, station1, station3, 13)); + } +} diff --git a/src/test/java/wooteco/subway/domain/SectionsTest.java b/src/test/java/wooteco/subway/domain/SectionsTest.java new file mode 100644 index 000000000..f102152fe --- /dev/null +++ b/src/test/java/wooteco/subway/domain/SectionsTest.java @@ -0,0 +1,279 @@ +package wooteco.subway.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SectionsTest { + + @Test + @DisplayName("생성 시 size가 0이면 예외가 발생한다.") + void createExceptionByEmptySize() { + assertThatThrownBy(() -> new Sections(new ArrayList<>())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("sections는 크기가 0으로는 생성할 수 없습니다."); + } + + @Test + @DisplayName("정렬된 Station을 반환할 수 있다.") + void calculateSortedStations() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Station station4 = new Station(4L, "배리"); + Sections sections = new Sections(List.of(new Section(3L, 1L, station3, station4, 4), + new Section(1L, 1L, station1, station2, 1), + new Section(2L, 1L, station2, station3, 2))); + + // when + List stations = sections.calculateSortedStations(); + + // then + assertThat(stations).contains(station1, station2, station3, station4); + } + + @Test + @DisplayName("Section을 추가할 때 상행, 하행역을 하나도 포함하지않으면 예외가 발생한다.") + void addSectionExceptionByNotFoundStation() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Station station4 = new Station(4L, "배리"); + Sections sections = new Sections(List.of(new Section(1L, 1L, station1, station2, 2))); + + // when & then + assertThatThrownBy(() -> sections.addSection(new Section(1L, station3, station4, 2))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("구간 추가는 기존의 상행역 하행역 중 하나를 포함해야합니다."); + } + + @Test + @DisplayName("이미 상행에서 하행으로 갈 수 있는 Section이면 예외가 발생한다.") + void addSectionExceptionByExistSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Sections sections = new Sections(List.of(new Section(1L, 1L, station1, station2, 2), + new Section(2L, 1L, station2, station3, 3))); + + // when & then + assertThatThrownBy(() -> sections.addSection(new Section(1L, station1, station3, 3))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("이미 상행에서 하행으로 갈 수 있는 구간이 존재합니다."); + } + + @Test + @DisplayName("입력된 Section의 하행역이 최상행역과 일치할 경우 단순히 추가만 한다.") + void addSectionByTopSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section section = new Section(1L, 1L, station2, station3, 3); + Section addSection = new Section(2L, 1L, station1, station2, 4); + + Sections sections = new Sections(List.of(section)); + Sections expectedSections = new Sections(List.of(section, addSection)); + + // when + sections.addSection(addSection); + + // then + assertThat(sections).isEqualTo(expectedSections); + } + + @Test + @DisplayName("입력된 Section의 상행역이 최하행역과 일치할 경우 단순히 추가만 한다.") + void addSectionByBottomSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section section = new Section(1L, 1L, station1, station2, 3); + Section addSection = new Section(2L, 1L, station2, station3, 4); + + Sections sections = new Sections(List.of(section)); + Sections expectedSections = new Sections(List.of(section, addSection)); + + // when + sections.addSection(addSection); + + // then + assertThat(sections).isEqualTo(expectedSections); + } + + @Test + @DisplayName("상행역이 일치하는 역의 사이에 들어갈 수 있다.") + void addSectionBetweenEqualsUpStation() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section addSection = new Section(1L, 1L, station1, station2, 3); + Sections sections = new Sections(List.of(new Section(2L, 1L, station1, station3, 10))); + + Sections expectedSections = new Sections(List.of(addSection, + new Section(2L, 1L, station2, station3, 7))); + + // when + sections.addSection(addSection); + + // when + assertThat(sections).isEqualTo(expectedSections); + } + + @ParameterizedTest + @ValueSource(ints = {2, 3}) + @DisplayName("상행역과 일치하는 역의 사이에 들어갈 때 더 크거나 같은 길이의 Section이면 예외가 발생한다.") + void addSectionBetweenEqualsUpStationExceptionByEqualsOrLargerDistance(int distance) { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section addSection = new Section(1L, 1L, station1, station2, distance); + Sections sections = new Sections(List.of(new Section(2L, 1L, station1, station3, 2))); + + // when & then + assertThatThrownBy(() -> sections.addSection(addSection)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("기존 길이보다 길거나 같은 구간은 중간에 추가될 수 없습니다."); + } + + @Test + @DisplayName("하행역이 일치하는 역의 사이에 들어갈 수 있다.") + void addSectionBetweenEqualsDownStation() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section addSection = new Section(1L, 1L, station2, station3, 3); + Sections sections = new Sections(List.of(new Section(2L, 1L, station1, station3, 10))); + + Sections expectedSections = new Sections(List.of(addSection, + new Section(2L, 1L, station1, station2, 7))); + + // when + sections.addSection(addSection); + + // when + assertThat(sections).isEqualTo(expectedSections); + } + + @ParameterizedTest + @ValueSource(ints = {2, 3}) + @DisplayName("하행역과 일치하는 역의 사이에 들어갈 때 더 크거나 같은 길이의 Section이면 예외가 발생한다.") + void addSectionBetweenEqualsDownStationExceptionByEqualsOrLargerDistance(int distance) { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section addSection = new Section(1L, 1L, station2, station3, distance); + Sections sections = new Sections(List.of(new Section(2L, 1L, station1, station3, 2))); + + // when & then + assertThatThrownBy(() -> sections.addSection(addSection)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("기존 길이보다 길거나 같은 구간은 중간에 추가될 수 없습니다."); + } + + @Test + @DisplayName("구간 제거 시 Station이 포함되지 않은 경우 예외가 발생한다.") + void removeSectionExceptionByNotFoundException() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station removeStation = new Station(3L, "오카라"); + Sections sections = new Sections(List.of(new Section(1L, 1L, station1, station2, 2))); + + // when & then + assertThatThrownBy(() -> sections.removeSection(removeStation)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("해당 역은 구간에 포함되어있지 않습니다."); + } + + @Test + @DisplayName("구간 제거 시 Section이 하나뿐이면 예외가 발생한다.") + void removeSectionExceptionByLimitSize() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Section section = new Section(1L, 1L, station1, station2, 2); + Sections sections = new Sections(List.of(section)); + + // when & then + assertThatThrownBy(() -> sections.removeSection(station2)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("구간이 하나뿐이어서 제거할 수 없습니다."); + } + + @Test + @DisplayName("입력된 구간이 최상행이라면 해당 구간만 제거된다.") + void removeTopSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section topSection = new Section(1L, 1L, station1, station2, 3); + Section bottomSection = new Section(2L, 1L, station2, station3, 4); + + Sections sections = new Sections(List.of(topSection, bottomSection)); + + // when + Section section = sections.removeSection(station1); + + // then + assertThat(section).isEqualTo(topSection); + } + + @Test + @DisplayName("입력된 구간이 최하행이라면 해당 구간만 제거된다.") + void removeBottomSection() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Section topSection = new Section(1L, 1L, station1, station2, 3); + Section bottomSection = new Section(2L, 1L, station2, station3, 4); + + Sections sections = new Sections(List.of(topSection, bottomSection)); + + // when + Section section = sections.removeSection(station3); + + // then + assertThat(section).isEqualTo(bottomSection); + } + + @Test + @DisplayName("입력된 구간이 중간에 있다면 다른 구간이 해당 구간을 연장한다.") + void removeMiddleSection() { + // given + Station topStation = new Station(1L, "오리"); + Station middleStation = new Station(2L, "배카라"); + Station bottomStation = new Station(3L, "오카라"); + + Section topSection = new Section(1L, 1L, topStation, middleStation, 3); + Section bottomSection = new Section(2L, 1L, middleStation, bottomStation, 4); + Sections sections = new Sections(List.of(topSection, bottomSection)); + + // when + sections.removeSection(middleStation); + + // then + assertThat(sections.getSections()).hasSize(1) + .extracting(Section::getId, Section::getUpStation, Section::getDownStation, Section::getDistance) + .containsExactly( + tuple(topSection.getId(), topStation, bottomStation, 7) + ); + } +} diff --git a/src/test/java/wooteco/subway/service/LineServiceTest.java b/src/test/java/wooteco/subway/service/LineServiceTest.java new file mode 100644 index 000000000..b39d4faa2 --- /dev/null +++ b/src/test/java/wooteco/subway/service/LineServiceTest.java @@ -0,0 +1,66 @@ +package wooteco.subway.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.dao.line.InmemoryLineDao; +import wooteco.subway.dao.section.InmemorySectionDao; +import wooteco.subway.dao.station.InmemoryStationDao; +import wooteco.subway.domain.Line; +import wooteco.subway.domain.Station; +import wooteco.subway.dto.line.LineSaveRequest; +import wooteco.subway.dto.line.LineUpdateRequest; +import wooteco.subway.exception.NotFoundException; + +class LineServiceTest { + + private final InmemoryLineDao lineDao = InmemoryLineDao.getInstance(); + private final InmemorySectionDao sectionDao = InmemorySectionDao.getInstance(); + private final InmemoryStationDao stationDao = InmemoryStationDao.getInstance(); + private final LineService lineService = new LineService(lineDao, stationDao, sectionDao); + + @AfterEach + void afterEach() { + lineDao.clear(); + sectionDao.clear(); + stationDao.clear(); + } + + @Test + @DisplayName("이미 존재하는 노선의 이름이 있을 때 예외가 발생한다.") + void saveExceptionByExistName() { + lineDao.save(new Line("신분당선", "bg-red-600")); + assertThatThrownBy(() -> lineService.save(new LineSaveRequest("신분당선", "bg-green-600", 1L, 2L, 2))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("이미 존재하는 노선 이름입니다."); + } + + @Test + @DisplayName("정상적으로 원하는 LineRequest를 저장하여 반환할 수 있다.") + void save() { + Station upStation = stationDao.findById(stationDao.save(new Station("오리"))); + Station downStation = stationDao.findById(stationDao.save(new Station("배카라"))); + LineSaveRequest lineSaveRequest = new LineSaveRequest("신분당선", "bg-red-600", upStation.getId(), downStation.getId(), 1); + + assertThat(lineService.save(lineSaveRequest)).isNotNull(); + } + + @Test + @DisplayName("존재하지 않는 id로 update하려할 경우 예외가 발생한다.") + void updateExceptionByNotFoundLine() { + assertThatThrownBy(() -> lineService.update(1L, new LineUpdateRequest("신분당선", "bg-green-600"))) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 Line입니다."); + } + + @Test + @DisplayName("존재하지 않는 id로 delete하려할 경우 예외가 발생한다.") + void deleteExceptionByNotFoundLine() { + assertThatThrownBy(() -> lineService.delete(1L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 Line입니다."); + } +} diff --git a/src/test/java/wooteco/subway/service/SectionServiceTest.java b/src/test/java/wooteco/subway/service/SectionServiceTest.java new file mode 100644 index 000000000..898bb9dd3 --- /dev/null +++ b/src/test/java/wooteco/subway/service/SectionServiceTest.java @@ -0,0 +1,70 @@ +package wooteco.subway.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.dao.line.InmemoryLineDao; +import wooteco.subway.dao.section.InmemorySectionDao; +import wooteco.subway.dao.station.InmemoryStationDao; +import wooteco.subway.domain.Line; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Station; +import wooteco.subway.dto.section.SectionSaveRequest; + +class SectionServiceTest { + + private final InmemoryLineDao lineDao = InmemoryLineDao.getInstance(); + private final InmemorySectionDao sectionDao = InmemorySectionDao.getInstance(); + private final InmemoryStationDao stationDao = InmemoryStationDao.getInstance(); + + private final SectionService sectionService = new SectionService(lineDao, sectionDao, stationDao); + + @AfterEach + void afterEach() { + lineDao.clear(); + sectionDao.clear(); + stationDao.clear(); + } + + @Test + @DisplayName("Section을 추가할 수 있다.") + void save() { + // given + long lineId = lineDao.save(new Line("신분당선", "bg-red-600")); + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + Station station3 = stationDao.findById(stationDao.save(new Station("오카라"))); + sectionDao.save(new Section(lineId, station1, station3, 10)); + + // when + int result = sectionService.save(lineId, new SectionSaveRequest(station1.getId(), station2.getId(), 3)); + + // then + assertThat(result).isEqualTo(result); + } + + @Test + @DisplayName("Station을 받아 구간을 삭제할 수 있다.") + void delete() { + // given + long lineId = lineDao.save(new Line("신분당선", "bg-red-600")); + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + Station station3 = stationDao.findById(stationDao.save(new Station("오카라"))); + sectionDao.save(new Section(lineId, station1, station2, 10)); + sectionDao.save(new Section(lineId, station2, station3, 10)); + + // when + sectionService.delete(lineId, station2.getId()); + + // then + assertThat(sectionDao.findAllByLineId(lineId)).hasSize(1) + .extracting(Section::getUpStation, Section::getDownStation) + .containsExactly( + tuple(station1, station3) + ); + } +} diff --git a/src/test/java/wooteco/subway/service/StationServiceTest.java b/src/test/java/wooteco/subway/service/StationServiceTest.java new file mode 100644 index 000000000..c04483a65 --- /dev/null +++ b/src/test/java/wooteco/subway/service/StationServiceTest.java @@ -0,0 +1,38 @@ +package wooteco.subway.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.dao.station.InmemoryStationDao; +import wooteco.subway.domain.Station; +import wooteco.subway.exception.NotFoundException; + +class StationServiceTest { + + private final InmemoryStationDao inmemoryStationDao = InmemoryStationDao.getInstance(); + private final StationService stationService = new StationService(inmemoryStationDao); + + @AfterEach + void afterEach() { + inmemoryStationDao.clear(); + } + + @Test + @DisplayName("이미 존재하는 역 이름이 있을 때 예외가 발생한다.") + void saveExceptionByDuplicatedName() { + inmemoryStationDao.save(new Station("오리")); + assertThatThrownBy(() -> stationService.save(new Station("오리"))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("이미 존재하는 역 이름입니다."); + } + + @Test + @DisplayName("존재하지 않는 id로 delete하려할 경우 예외가 발생한다.") + void deleteExceptionByNotFoundLine() { + assertThatThrownBy(() -> stationService.delete(1L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 Station입니다."); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 000000000..0307b30b9 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,9 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: + h2: + console: + enabled: true diff --git a/src/test/resources/schema-truncate.sql b/src/test/resources/schema-truncate.sql new file mode 100644 index 000000000..590d4c6e4 --- /dev/null +++ b/src/test/resources/schema-truncate.sql @@ -0,0 +1,3 @@ +TRUNCATE TABLE STATION; +TRUNCATE TABLE LINE; +TRUNCATE TABLE SECTION; diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 000000000..d44fdc202 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,28 @@ +DROP TABLE IF EXISTS STATION; +DROP TABLE IF EXISTS LINE; +DROP TABLE IF EXISTS SECTION; + +CREATE TABLE STATION +( + id bigint auto_increment not null, + name varchar(255) not null unique, + primary key (id) +); + +CREATE TABLE LINE +( + id bigint auto_increment not null, + name varchar(255) not null unique, + color varchar(20) not null, + primary key (id) +); + +create table SECTION +( + 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, + primary key (id) +); From 36ae4b4c14c86cdebe96b34916d09b58e91bda4f Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 14:58:29 +0900 Subject: [PATCH 02/28] =?UTF-8?q?docs:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 57 ++++++++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 52d339efb..e43651e72 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,18 @@ -

- -

-

- - Website - - GitHub -

- -
- -# 지하철 노선도 미션 -스프링 과정 실습을 위한 지하철 노선도 애플리케이션 - -
- -## 🚀 Getting Started -### Usage -#### application 구동 -``` -./gradlew bootRun -``` -
- -## ✏️ Code Review Process -[텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) - -
- -## 🐞 Bug Report - -버그를 발견한다면, [Issues](https://github.com/woowacourse/atdd-subway-path/issues) 에 등록해주세요 :) - -
- -## 📝 License - -This project is [MIT](https://github.com/woowacourse/atdd-subway-path/blob/master/LICENSE) licensed. +### step1 요구사항 +- [ERROR] 출발역과 도착역이 Sections에 포함되지 않으면 예외가 발생한다. +- [ERROR] 출발역에서 도착역으로 가는 경로가 포함되어 있지 않으면 예외가 발생한다. + +- 출발역과 도착역 그리고 전체 Section정보를 받아 경로를 만드는 전략이 존재한다. +- 전략의 경우 다익스트라 라이브러리를 사용한 전략이 존재한다. + - 정점(vertex) : 지하철 역 (Station) + - 간선(edge) : 지하철역 연결정보(Section) + - 가중치(edgeWeight) : 거리 + - 최단 거리 기준 조회 시 가중치를 거리로 설정한다. + - 들어온 Station List 정보를 반환한다. +- Path + - path는 거리와 station정보를 가지고 있다. + - path에서는 가지고 있는 stations의 모든 거리를 계산할 수 있다. + - path에서는 거리에 따른 돈 계산을 반환할 수 있다. + - 기본 운임 (10km 이내)는 1,250원이다. + - 10~50km까지는 5km마다 100원을 추가한다. + - 50km 초과 시 8km까지 마다 100원을 추가한다. From eb2e50409fd6aaccd99626bd2abcc6e37fc95f2d Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 14:59:26 +0900 Subject: [PATCH 03/28] =?UTF-8?q?feat:=20Section=20=EB=82=B4=20station=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=20=EC=B2=B4=ED=81=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wooteco/subway/domain/Sections.java | 11 +++++++++++ .../wooteco/subway/domain/SectionsTest.java | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/main/java/wooteco/subway/domain/Sections.java b/src/main/java/wooteco/subway/domain/Sections.java index 96c088ecd..5587d20c3 100644 --- a/src/main/java/wooteco/subway/domain/Sections.java +++ b/src/main/java/wooteco/subway/domain/Sections.java @@ -182,6 +182,17 @@ private Section upperSection(final Section section) { .orElseThrow(notFoundSectionSupplier()); } + public void checkExistStations(final Station source, final Station target) { + if (isNotExistStation(source) || isNotExistStation(target)) { + throw new IllegalStateException("현재 Sections에 존재하지 않는 station입니다."); + } + } + + private boolean isNotExistStation(final Station station) { + return sections.stream() + .noneMatch(section -> section.containsStation(station)); + } + private Section calculateLastSection() { return calculateLastSection(findAnySection()); } diff --git a/src/test/java/wooteco/subway/domain/SectionsTest.java b/src/test/java/wooteco/subway/domain/SectionsTest.java index f102152fe..58a06aff0 100644 --- a/src/test/java/wooteco/subway/domain/SectionsTest.java +++ b/src/test/java/wooteco/subway/domain/SectionsTest.java @@ -276,4 +276,22 @@ void removeMiddleSection() { tuple(topSection.getId(), topStation, bottomStation, 7) ); } + + @Test + @DisplayName("station이 포함되어있지 않으면 예외가 발생한다.") + void checkExistStationsExceptionByNotContain() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Station station4 = new Station(3L, "레넌"); + + Section section = new Section(1L, 1L, station1, station2, 3); + Sections sections = new Sections(List.of(section)); + + // when + assertThatThrownBy(() -> sections.checkExistStations(station3, station4)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("현재 Sections에 존재하지 않는 station입니다."); + } } From 3e6bac9e8b493d1a685d406f81cfc591ff4440a6 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 15:01:57 +0900 Subject: [PATCH 04/28] =?UTF-8?q?feat:=20=EA=B8=B8=EC=B0=BE=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 14 ++++++++ .../FindDijkstraShortestPathStrategy.java | 14 ++++++++ .../domain/strategy/FindPathStrategy.java | 11 ++++++ .../FindDijkstraShortestPathStrategyTest.java | 34 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 src/main/java/wooteco/subway/domain/Path.java create mode 100644 src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java create mode 100644 src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java create mode 100644 src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java new file mode 100644 index 000000000..c7f99f113 --- /dev/null +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -0,0 +1,14 @@ +package wooteco.subway.domain; + +import java.util.List; + +public class Path { + + private final List stations; + private final int distance; + + public Path(final List stations, final int distance) { + this.stations = stations; + this.distance = distance; + } +} diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java new file mode 100644 index 000000000..b5c9138cf --- /dev/null +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -0,0 +1,14 @@ +package wooteco.subway.domain.strategy; + +import wooteco.subway.domain.Path; +import wooteco.subway.domain.Sections; +import wooteco.subway.domain.Station; + +public class FindDijkstraShortestPathStrategy implements FindPathStrategy { + + @Override + public Path findPath(final Station source, final Station target, final Sections sections) { + sections.checkExistStations(source, target); + return null; + } +} diff --git a/src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java new file mode 100644 index 000000000..6ecef682a --- /dev/null +++ b/src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java @@ -0,0 +1,11 @@ +package wooteco.subway.domain.strategy; + +import wooteco.subway.domain.Path; +import wooteco.subway.domain.Sections; +import wooteco.subway.domain.Station; + +public interface FindPathStrategy { + + Path findPath(Station source, Station target, Sections sections); + +} diff --git a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java new file mode 100644 index 000000000..d941946de --- /dev/null +++ b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java @@ -0,0 +1,34 @@ +package wooteco.subway.domain.strategy; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Sections; +import wooteco.subway.domain.Station; + +class FindDijkstraShortestPathStrategyTest { + + @Test + @DisplayName("source와 target이 존재하지 않으면 예외 발생한다.") + void findPathExceptionByNotExistStations() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Station station4 = new Station(3L, "레넌"); + + Section section = new Section(1L, 1L, station1, station2, 3); + Sections sections = new Sections(List.of(section)); + + FindPathStrategy findPathStrategy = new FindDijkstraShortestPathStrategy(); + + // when & then + assertThatThrownBy(() -> findPathStrategy.findPath(station3, station4, sections)) + .isInstanceOf(IllegalStateException.class); + } + +} From 585b4b1e6c8651dffddddd2b6cdecc83fe23b016 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 15:23:30 +0900 Subject: [PATCH 05/28] =?UTF-8?q?feat:=20=EC=B5=9C=EB=8B=A8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 8 ++++++ .../java/wooteco/subway/domain/Sections.java | 16 ++++++++++++ .../FindDijkstraShortestPathStrategy.java | 22 +++++++++++++++- .../FindDijkstraShortestPathStrategyTest.java | 26 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index c7f99f113..cd115aacd 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -11,4 +11,12 @@ public Path(final List stations, final int distance) { this.stations = stations; this.distance = distance; } + + public List getStations() { + return stations; + } + + public int getDistance() { + return distance; + } } diff --git a/src/main/java/wooteco/subway/domain/Sections.java b/src/main/java/wooteco/subway/domain/Sections.java index 5587d20c3..04df2abd8 100644 --- a/src/main/java/wooteco/subway/domain/Sections.java +++ b/src/main/java/wooteco/subway/domain/Sections.java @@ -1,10 +1,13 @@ package wooteco.subway.domain; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; import wooteco.subway.exception.NotFoundException; public class Sections { @@ -193,6 +196,19 @@ private boolean isNotExistStation(final Station station) { .noneMatch(section -> section.containsStation(station)); } + public List getAllStations() { + List upStations = sections.stream() + .map(Section::getUpStation) + .collect(Collectors.toList()); + List downStations = sections.stream() + .map(Section::getDownStation) + .collect(Collectors.toList()); + return Stream.of(upStations, downStations) + .flatMap(Collection::stream) + .distinct() + .collect(Collectors.toList()); + } + private Section calculateLastSection() { return calculateLastSection(findAnySection()); } diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java index b5c9138cf..3c622a804 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -1,6 +1,15 @@ package wooteco.subway.domain.strategy; +import java.util.List; +import org.jgrapht.Graph; +import org.jgrapht.GraphPath; +import org.jgrapht.alg.shortestpath.DijkstraShortestPath; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.WeightedMultigraph; import wooteco.subway.domain.Path; +import wooteco.subway.domain.Section; import wooteco.subway.domain.Sections; import wooteco.subway.domain.Station; @@ -9,6 +18,17 @@ public class FindDijkstraShortestPathStrategy implements FindPathStrategy { @Override public Path findPath(final Station source, final Station target, final Sections sections) { sections.checkExistStations(source, target); - return null; + WeightedMultigraph graph = new WeightedMultigraph<>(DefaultWeightedEdge.class); + List allStations = sections.getAllStations(); + for (Station station : allStations) { + graph.addVertex(station); + } + List
allSections = sections.getSections(); + for (Section section : allSections) { + graph.setEdgeWeight(graph.addEdge(section.getUpStation(), section.getDownStation()), section.getDistance()); + } + + GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); + return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); } } diff --git a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java index d941946de..fa1ac5849 100644 --- a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java +++ b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java @@ -1,11 +1,13 @@ package wooteco.subway.domain.strategy; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import wooteco.subway.domain.Path; import wooteco.subway.domain.Section; import wooteco.subway.domain.Sections; import wooteco.subway.domain.Station; @@ -31,4 +33,28 @@ void findPathExceptionByNotExistStations() { .isInstanceOf(IllegalStateException.class); } + @Test + @DisplayName("최단거리 정보를 반환할 수 있다.") + void findPath() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Station station4 = new Station(4L, "레넌"); + + Sections sections = new Sections( + List.of( + new Section(1L, 1L, station1, station2, 2), + new Section(2L, 1L, station2, station3, 2), + new Section(3L, 1L, station1, station4, 3), + new Section(4L, 1L, station4, station3, 3))); + + FindPathStrategy findPathStrategy = new FindDijkstraShortestPathStrategy(); + Path path = findPathStrategy.findPath(station1, station3, sections); + + assertAll( + () -> assertThat(path.getStations()).containsExactly(station1, station2, station3), + () -> assertThat(path.getDistance()).isEqualTo(4) + ); + } } From 345aca0abb009dd9d8bfa584e1b23cedd8bfff31 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 15:24:54 +0900 Subject: [PATCH 06/28] =?UTF-8?q?refactor:=20=EB=A9=94=EC=86=8C=EB=93=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindDijkstraShortestPathStrategy.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java index 3c622a804..302bebad8 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -1,11 +1,8 @@ package wooteco.subway.domain.strategy; import java.util.List; -import org.jgrapht.Graph; import org.jgrapht.GraphPath; import org.jgrapht.alg.shortestpath.DijkstraShortestPath; -import org.jgrapht.graph.DefaultDirectedGraph; -import org.jgrapht.graph.DefaultEdge; import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.graph.WeightedMultigraph; import wooteco.subway.domain.Path; @@ -19,16 +16,26 @@ public class FindDijkstraShortestPathStrategy implements FindPathStrategy { public Path findPath(final Station source, final Station target, final Sections sections) { sections.checkExistStations(source, target); WeightedMultigraph graph = new WeightedMultigraph<>(DefaultWeightedEdge.class); + addVertextStation(sections, graph); + addEdgeWeightStation(sections, graph); + + GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); + return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); + } + + private void addVertextStation(final Sections sections, + final WeightedMultigraph graph) { List allStations = sections.getAllStations(); for (Station station : allStations) { graph.addVertex(station); } + } + + private void addEdgeWeightStation(final Sections sections, + final WeightedMultigraph graph) { List
allSections = sections.getSections(); for (Section section : allSections) { graph.setEdgeWeight(graph.addEdge(section.getUpStation(), section.getDownStation()), section.getDistance()); } - - GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); - return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); } } From 986dfca1c3a7f4ed95c55a6a5207998fb36e1d28 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 15:38:54 +0900 Subject: [PATCH 07/28] =?UTF-8?q?feat:=20findAll=20Sections=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/dao/section/InmemorySectionDao.java | 6 ++++++ .../wooteco/subway/dao/section/JdbcSectionDao.java | 10 ++++++++++ .../wooteco/subway/dao/section/SectionDao.java | 2 ++ .../subway/dao/section/InmemorySectionDaoTest.java | 14 ++++++++++++++ .../subway/dao/section/JdbcSectionDaoTest.java | 14 ++++++++++++++ 5 files changed, 46 insertions(+) diff --git a/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java b/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java index 36f85fed0..9ec6e0746 100644 --- a/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java +++ b/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java @@ -1,6 +1,7 @@ package wooteco.subway.dao.section; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,6 +51,11 @@ public List
findAllByLineId(final long lineId) { .collect(Collectors.toList()); } + @Override + public List
findAll() { + return new ArrayList<>(sections.values()); + } + @Override public int updateSections(final List
sections) { clear(); diff --git a/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java b/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java index 32199c141..a2b091ab8 100644 --- a/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java +++ b/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java @@ -57,6 +57,16 @@ public List
findAllByLineId(final long lineId) { return jdbcTemplate.query(sql, sectionRowMapper, lineId); } + @Override + public List
findAll() { + final String sql = "select s.id as id, s.line_id as line_id, us.id as us_id, us.name as us_name," + + "ds.id as ds_id, ds.name as ds_name, s.distance as distance from SECTION as s " + + "join STATION as us on us.id = s.up_station_id " + + "join STATION as ds on ds.id = s.down_station_id"; + + return jdbcTemplate.query(sql, sectionRowMapper); + } + @Override public int updateSections(final List
sections) { final String sql = "update SECTION set up_station_id = ?, down_station_id = ?, distance = ? where id = ?"; diff --git a/src/main/java/wooteco/subway/dao/section/SectionDao.java b/src/main/java/wooteco/subway/dao/section/SectionDao.java index 803ce9f54..0ac5284d7 100644 --- a/src/main/java/wooteco/subway/dao/section/SectionDao.java +++ b/src/main/java/wooteco/subway/dao/section/SectionDao.java @@ -9,6 +9,8 @@ public interface SectionDao { List
findAllByLineId(long lineId); + List
findAll(); + int updateSections(List
sections); int delete(long sectionId); diff --git a/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java b/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java index 69675602f..9db8246d1 100644 --- a/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java +++ b/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java @@ -52,6 +52,20 @@ void findAllByLineId() { assertThat(sectionDao.findAllByLineId(lineId)).hasSize(2); } + @Test + @DisplayName("모든 Section을 조회할 수 있다.") + void findAll() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + Station station3 = stationDao.findById(stationDao.save(new Station("오카라"))); + + sectionDao.save(new Section(id, station1, station2, 2)); + sectionDao.save(new Section(id, station2, station3, 2)); + + assertThat(sectionDao.findAll()).hasSize(2); + } + @Test @DisplayName("Section을 삭제할 수 있다.") void delete() { diff --git a/src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java b/src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java index 6228b258a..08765c407 100644 --- a/src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java +++ b/src/test/java/wooteco/subway/dao/section/JdbcSectionDaoTest.java @@ -64,6 +64,20 @@ void findAllByLineId() { assertThat(sectionDao.findAllByLineId(id)).hasSize(2); } + @Test + @DisplayName("모든 Section을 조회할 수 있다.") + void findAll() { + long id = lineDao.save(new Line("신분당선", "bg-red-600")); + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + Station station3 = stationDao.findById(stationDao.save(new Station("오카라"))); + + sectionDao.save(new Section(id, station1, station2, 2)); + sectionDao.save(new Section(id, station2, station3, 2)); + + assertThat(sectionDao.findAll()).hasSize(2); + } + @Test @DisplayName("Sections를 업데이트할 수 있다.") void updateSections() { From ef571d013ff154875e11c0e4f7223317e81ccd6a Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 15:58:00 +0900 Subject: [PATCH 08/28] =?UTF-8?q?feat:=20PathService=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/PathConfiguration.java | 15 ++++++ .../subway/dto/path/PathFindRequest.java | 29 ++++++++++ .../wooteco/subway/service/PathService.java | 33 ++++++++++++ .../subway/service/PathServiceTest.java | 53 +++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 src/main/java/wooteco/subway/configuration/PathConfiguration.java create mode 100644 src/main/java/wooteco/subway/dto/path/PathFindRequest.java create mode 100644 src/main/java/wooteco/subway/service/PathService.java create mode 100644 src/test/java/wooteco/subway/service/PathServiceTest.java diff --git a/src/main/java/wooteco/subway/configuration/PathConfiguration.java b/src/main/java/wooteco/subway/configuration/PathConfiguration.java new file mode 100644 index 000000000..7e25ebdb9 --- /dev/null +++ b/src/main/java/wooteco/subway/configuration/PathConfiguration.java @@ -0,0 +1,15 @@ +package wooteco.subway.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import wooteco.subway.domain.strategy.FindDijkstraShortestPathStrategy; +import wooteco.subway.domain.strategy.FindPathStrategy; + +@Configuration +public class PathConfiguration { + + @Bean + public FindPathStrategy findPathStrategy() { + return new FindDijkstraShortestPathStrategy(); + } +} diff --git a/src/main/java/wooteco/subway/dto/path/PathFindRequest.java b/src/main/java/wooteco/subway/dto/path/PathFindRequest.java new file mode 100644 index 000000000..dc6c95cc9 --- /dev/null +++ b/src/main/java/wooteco/subway/dto/path/PathFindRequest.java @@ -0,0 +1,29 @@ +package wooteco.subway.dto.path; + +public class PathFindRequest { + + private long source; + private long target; + private int age; + + private PathFindRequest() { + } + + public PathFindRequest(final long source, final long target, final int age) { + this.source = source; + this.target = target; + this.age = age; + } + + public long getSource() { + return source; + } + + public long getTarget() { + return target; + } + + public int getAge() { + return age; + } +} diff --git a/src/main/java/wooteco/subway/service/PathService.java b/src/main/java/wooteco/subway/service/PathService.java new file mode 100644 index 000000000..843e0a821 --- /dev/null +++ b/src/main/java/wooteco/subway/service/PathService.java @@ -0,0 +1,33 @@ +package wooteco.subway.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.subway.dao.section.SectionDao; +import wooteco.subway.dao.station.StationDao; +import wooteco.subway.domain.Path; +import wooteco.subway.domain.Sections; +import wooteco.subway.domain.Station; +import wooteco.subway.domain.strategy.FindPathStrategy; +import wooteco.subway.dto.path.PathFindRequest; + +@Service +@Transactional(readOnly = true) +public class PathService { + + private final SectionDao sectionDao; + private final StationDao stationDao; + private final FindPathStrategy findPathStrategy; + + public PathService(final SectionDao sectionDao, final StationDao stationDao, final FindPathStrategy findPathStrategy) { + this.sectionDao = sectionDao; + this.stationDao = stationDao; + this.findPathStrategy = findPathStrategy; + } + + public Path findPath(final PathFindRequest pathFindRequest) { + Sections sections = new Sections(sectionDao.findAll()); + Station source = stationDao.findById(pathFindRequest.getSource()); + Station target = stationDao.findById(pathFindRequest.getTarget()); + return findPathStrategy.findPath(source, target, sections); + } +} diff --git a/src/test/java/wooteco/subway/service/PathServiceTest.java b/src/test/java/wooteco/subway/service/PathServiceTest.java new file mode 100644 index 000000000..e90a4643e --- /dev/null +++ b/src/test/java/wooteco/subway/service/PathServiceTest.java @@ -0,0 +1,53 @@ +package wooteco.subway.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.subway.dao.section.InmemorySectionDao; +import wooteco.subway.dao.station.InmemoryStationDao; +import wooteco.subway.domain.Path; +import wooteco.subway.domain.Section; +import wooteco.subway.domain.Station; +import wooteco.subway.domain.strategy.FindDijkstraShortestPathStrategy; +import wooteco.subway.dto.path.PathFindRequest; + +class PathServiceTest { + + private final InmemorySectionDao sectionDao = InmemorySectionDao.getInstance(); + private final InmemoryStationDao stationDao = InmemoryStationDao.getInstance(); + private final PathService pathService = new PathService(sectionDao, stationDao, + new FindDijkstraShortestPathStrategy()); + + @AfterEach + void afterEach() { + sectionDao.clear(); + stationDao.clear(); + } + + @Test + @DisplayName("경로를 조회할 수 있다.") + void findPath() { + // given + Station station1 = stationDao.findById(stationDao.save(new Station("오리"))); + Station station2 = stationDao.findById(stationDao.save(new Station("배카라"))); + Station station3 = stationDao.findById(stationDao.save(new Station("오카라"))); + Station station4 = stationDao.findById(stationDao.save(new Station("레넌"))); + + sectionDao.save(new Section(1L, station1, station2, 2)); + sectionDao.save(new Section(1L, station2, station3, 2)); + sectionDao.save(new Section(1L, station1, station4, 3)); + sectionDao.save(new Section(1L, station4, station3, 3)); + + // when + Path path = pathService.findPath(new PathFindRequest(1, 3, 15)); + + // then + assertAll( + () -> assertThat(path.getStations()).containsExactly(station1, station2, station3), + () -> assertThat(path.getDistance()).isEqualTo(4) + ); + } +} From 587601fd8b0e9a6f646c70efa9acb6572301fead Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:20:00 +0900 Subject: [PATCH 09/28] =?UTF-8?q?feat:=20GET=20"/paths"=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=20api=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wooteco/subway/service/PathService.java | 6 +++--- .../acceptance/SectionAcceptanceTest.java | 2 +- .../FindDijkstraShortestPathStrategyTest.java | 6 +++--- .../wooteco/subway/service/PathServiceTest.java | 17 ++++++++++++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/wooteco/subway/service/PathService.java b/src/main/java/wooteco/subway/service/PathService.java index 843e0a821..d50d0fbb9 100644 --- a/src/main/java/wooteco/subway/service/PathService.java +++ b/src/main/java/wooteco/subway/service/PathService.java @@ -4,11 +4,11 @@ import org.springframework.transaction.annotation.Transactional; import wooteco.subway.dao.section.SectionDao; import wooteco.subway.dao.station.StationDao; -import wooteco.subway.domain.Path; import wooteco.subway.domain.Sections; import wooteco.subway.domain.Station; import wooteco.subway.domain.strategy.FindPathStrategy; import wooteco.subway.dto.path.PathFindRequest; +import wooteco.subway.dto.path.PathFindResponse; @Service @Transactional(readOnly = true) @@ -24,10 +24,10 @@ public PathService(final SectionDao sectionDao, final StationDao stationDao, fin this.findPathStrategy = findPathStrategy; } - public Path findPath(final PathFindRequest pathFindRequest) { + public PathFindResponse findPath(final PathFindRequest pathFindRequest) { Sections sections = new Sections(sectionDao.findAll()); Station source = stationDao.findById(pathFindRequest.getSource()); Station target = stationDao.findById(pathFindRequest.getTarget()); - return findPathStrategy.findPath(source, target, sections); + return PathFindResponse.from(findPathStrategy.findPath(source, target, sections)); } } diff --git a/src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java index cfef23a97..46f2698ad 100644 --- a/src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/SectionAcceptanceTest.java @@ -19,7 +19,7 @@ public class SectionAcceptanceTest extends AcceptanceTest { - private ExtractableResponse postSections(final Long lineId, final SectionSaveRequest sectionSaveRequest) { + public static ExtractableResponse postSections(final Long lineId, final SectionSaveRequest sectionSaveRequest) { return RestAssured.given().log().all() .body(sectionSaveRequest) .contentType(MediaType.APPLICATION_JSON_VALUE) diff --git a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java index fa1ac5849..2c6e541d9 100644 --- a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java +++ b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -46,8 +46,8 @@ void findPath() { List.of( new Section(1L, 1L, station1, station2, 2), new Section(2L, 1L, station2, station3, 2), - new Section(3L, 1L, station1, station4, 3), - new Section(4L, 1L, station4, station3, 3))); + new Section(3L, 2L, station1, station4, 3), + new Section(4L, 2L, station4, station3, 3))); FindPathStrategy findPathStrategy = new FindDijkstraShortestPathStrategy(); Path path = findPathStrategy.findPath(station1, station3, sections); diff --git a/src/test/java/wooteco/subway/service/PathServiceTest.java b/src/test/java/wooteco/subway/service/PathServiceTest.java index e90a4643e..8219462c9 100644 --- a/src/test/java/wooteco/subway/service/PathServiceTest.java +++ b/src/test/java/wooteco/subway/service/PathServiceTest.java @@ -1,6 +1,7 @@ package wooteco.subway.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.Assertions.assertAll; import org.junit.jupiter.api.AfterEach; @@ -8,11 +9,11 @@ import org.junit.jupiter.api.Test; import wooteco.subway.dao.section.InmemorySectionDao; import wooteco.subway.dao.station.InmemoryStationDao; -import wooteco.subway.domain.Path; import wooteco.subway.domain.Section; import wooteco.subway.domain.Station; import wooteco.subway.domain.strategy.FindDijkstraShortestPathStrategy; import wooteco.subway.dto.path.PathFindRequest; +import wooteco.subway.dto.path.PathFindResponse; class PathServiceTest { @@ -38,15 +39,21 @@ void findPath() { sectionDao.save(new Section(1L, station1, station2, 2)); sectionDao.save(new Section(1L, station2, station3, 2)); - sectionDao.save(new Section(1L, station1, station4, 3)); - sectionDao.save(new Section(1L, station4, station3, 3)); + sectionDao.save(new Section(2L, station1, station4, 3)); + sectionDao.save(new Section(2L, station4, station3, 3)); // when - Path path = pathService.findPath(new PathFindRequest(1, 3, 15)); + PathFindResponse path = pathService.findPath(new PathFindRequest(1, 3, 15)); // then assertAll( - () -> assertThat(path.getStations()).containsExactly(station1, station2, station3), + () -> assertThat(path.getStations()) + .extracting("id", "name") + .containsExactly( + tuple(1L, "오리"), + tuple(2L, "배카라"), + tuple(3L, "오카라") + ), () -> assertThat(path.getDistance()).isEqualTo(4) ); } From 57f7d6c1be12ed9dd4ebb30448d6747b986dc123 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:20:07 +0900 Subject: [PATCH 10/28] =?UTF-8?q?feat:=20GET=20"/paths"=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=20api=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/controller/PathFindController.java | 25 ++++++++ .../subway/dto/path/PathFindResponse.java | 42 +++++++++++++ .../acceptance/PathFindAcceptanceTest.java | 59 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 src/main/java/wooteco/subway/controller/PathFindController.java create mode 100644 src/main/java/wooteco/subway/dto/path/PathFindResponse.java create mode 100644 src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java diff --git a/src/main/java/wooteco/subway/controller/PathFindController.java b/src/main/java/wooteco/subway/controller/PathFindController.java new file mode 100644 index 000000000..ff33e97e9 --- /dev/null +++ b/src/main/java/wooteco/subway/controller/PathFindController.java @@ -0,0 +1,25 @@ +package wooteco.subway.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RestController; +import wooteco.subway.dto.path.PathFindRequest; +import wooteco.subway.dto.path.PathFindResponse; +import wooteco.subway.service.PathService; + +@RestController +public class PathFindController { + + private final PathService pathService; + + public PathFindController(final PathService pathService) { + this.pathService = pathService; + } + + @GetMapping("/paths") + public ResponseEntity findPath(@ModelAttribute PathFindRequest pathFindRequest) { + PathFindResponse response = pathService.findPath(pathFindRequest); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/wooteco/subway/dto/path/PathFindResponse.java b/src/main/java/wooteco/subway/dto/path/PathFindResponse.java new file mode 100644 index 000000000..0b9646bdc --- /dev/null +++ b/src/main/java/wooteco/subway/dto/path/PathFindResponse.java @@ -0,0 +1,42 @@ +package wooteco.subway.dto.path; + +import java.util.List; +import java.util.stream.Collectors; +import wooteco.subway.domain.Path; +import wooteco.subway.dto.station.StationResponse; + +public class PathFindResponse { + + private List stations; + private int distance; + private int fare; + + private PathFindResponse() { + } + + public PathFindResponse(final List stations, final int distance, final int fare) { + this.stations = stations; + this.distance = distance; + this.fare = fare; + } + + public static PathFindResponse from(Path path) { + List stationResponses = path.getStations() + .stream() + .map(StationResponse::from) + .collect(Collectors.toList()); + return new PathFindResponse(stationResponses, path.getDistance(), 0); + } + + public List getStations() { + return stations; + } + + public int getDistance() { + return distance; + } + + public int getFare() { + return fare; + } +} diff --git a/src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java new file mode 100644 index 000000000..c4f661dd5 --- /dev/null +++ b/src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java @@ -0,0 +1,59 @@ +package wooteco.subway.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.subway.acceptance.LineAcceptanceTest.postLines; +import static wooteco.subway.acceptance.SectionAcceptanceTest.postSections; +import static wooteco.subway.acceptance.StationAcceptanceTest.postStations; + +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 wooteco.subway.dto.line.LineResponse; +import wooteco.subway.dto.line.LineSaveRequest; +import wooteco.subway.dto.section.SectionSaveRequest; +import wooteco.subway.dto.station.StationResponse; +import wooteco.subway.dto.station.StationSaveRequest; + +public class PathFindAcceptanceTest extends AcceptanceTest { + + private ExtractableResponse getPath(final Long stationId1, final Long stationId3) { + return RestAssured.given().log().all() + .param("source", stationId1) + .param("target", stationId3) + .param("age", 15) + .when() + .get("/paths") + .then().log().all() + .extract(); + } + + @Test + @DisplayName("경로를 조회한다.") + void findPath() { + // given + Long stationId1 = postStations(new StationSaveRequest("강남역")) + .as(StationResponse.class) + .getId(); + Long stationId2 = postStations(new StationSaveRequest("역삼역")) + .as(StationResponse.class) + .getId(); + Long stationId3 = postStations(new StationSaveRequest("선릉역")) + .as(StationResponse.class) + .getId(); + Long lineId = postLines(new LineSaveRequest("신분당선", "bg-red-600", stationId1, stationId3, 10)) + .as(LineResponse.class) + .getId(); + postSections(lineId, new SectionSaveRequest(stationId2, stationId3, 6)); + + // when + ExtractableResponse response = getPath(stationId1, stationId3); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + +} From b2529abaaf537fb3e9c828caefcc4a31b084c125 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:25:54 +0900 Subject: [PATCH 11/28] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20=EC=9A=B4?= =?UTF-8?q?=EC=9E=84=20=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 4 ++++ .../java/wooteco/subway/domain/PathTest.java | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/test/java/wooteco/subway/domain/PathTest.java diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index cd115aacd..e930adeae 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -12,6 +12,10 @@ public Path(final List stations, final int distance) { this.distance = distance; } + public int calculateFare() { + return 1250; + } + public List getStations() { return stations; } diff --git a/src/test/java/wooteco/subway/domain/PathTest.java b/src/test/java/wooteco/subway/domain/PathTest.java new file mode 100644 index 000000000..a2d28d2f4 --- /dev/null +++ b/src/test/java/wooteco/subway/domain/PathTest.java @@ -0,0 +1,21 @@ +package wooteco.subway.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PathTest { + + private final List stations = List.of(new Station(1L, "강남역"), new Station(2L, "선릉역")); + + @Test + @DisplayName("10km 이하일 때 기본 운임은 1250원이다.") + void calcualteDefaultFare() { + Path path = new Path(stations, 10); + + assertThat(path.calculateFare()).isEqualTo(1250); + } + +} From 5b3de4b2b57322af9b2350e5b2b9567ad500e55b Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:36:10 +0900 Subject: [PATCH 12/28] =?UTF-8?q?feat:=2050km=20=EC=9A=B4=EC=9E=84=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 14 +++++++++++++- src/test/java/wooteco/subway/domain/PathTest.java | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index e930adeae..d4265cbf3 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -4,6 +4,8 @@ public class Path { + private static final int DEFAULT_FARE = 1250; + private final List stations; private final int distance; @@ -13,7 +15,17 @@ public Path(final List stations, final int distance) { } public int calculateFare() { - return 1250; + if (distance <= 10) { + return DEFAULT_FARE; + } + if (distance <= 50) { + return DEFAULT_FARE + calculateOverFare(distance - 10, 5, 100); + } + return 0; + } + + private int calculateOverFare(int distance, int unitDistance, int overFare) { + return (int) ((Math.ceil((distance - 1) / unitDistance) + 1) * overFare); } public List getStations() { diff --git a/src/test/java/wooteco/subway/domain/PathTest.java b/src/test/java/wooteco/subway/domain/PathTest.java index a2d28d2f4..8e25ae0f8 100644 --- a/src/test/java/wooteco/subway/domain/PathTest.java +++ b/src/test/java/wooteco/subway/domain/PathTest.java @@ -18,4 +18,11 @@ void calcualteDefaultFare() { assertThat(path.calculateFare()).isEqualTo(1250); } + @Test + @DisplayName("50km 이하일 때 5km 마다 100원 추가된다.") + void calculate50Fare() { + Path path = new Path(stations, 50); + + assertThat(path.calculateFare()).isEqualTo(2050); + } } From d1fabc877c3c528b6b6178bab5e7bd079717ee92 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:38:29 +0900 Subject: [PATCH 13/28] =?UTF-8?q?refactor:=20Path=20=EB=A7=A4=EC=A7=81?= =?UTF-8?q?=EB=84=98=EB=B2=84=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index d4265cbf3..47f4a98e8 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -5,6 +5,11 @@ public class Path { private static final int DEFAULT_FARE = 1250; + private static final int DEFAULT_FARE_DISTANCE = 10; + + private static final int FIRST_ADDITIONAL_UNIT_FARE = 100; + private static final int FIRST_ADDITIONAL_UNIT_DISTANCE = 5; + private static final int FIRST_ADDITIONAL_FARE_DISTANCE = 50; private final List stations; private final int distance; @@ -15,11 +20,11 @@ public Path(final List stations, final int distance) { } public int calculateFare() { - if (distance <= 10) { + if (distance <= DEFAULT_FARE_DISTANCE) { return DEFAULT_FARE; } - if (distance <= 50) { - return DEFAULT_FARE + calculateOverFare(distance - 10, 5, 100); + if (distance <= FIRST_ADDITIONAL_FARE_DISTANCE) { + return DEFAULT_FARE + calculateOverFare(distance - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, FIRST_ADDITIONAL_UNIT_FARE); } return 0; } From b02911ae3c8aa695ac80216762d4c7400e99a335 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:44:12 +0900 Subject: [PATCH 14/28] =?UTF-8?q?feat:=2050km=20=EC=B4=88=EA=B3=BC?= =?UTF-8?q?=EA=B5=AC=EA=B0=84=20=EC=9A=B4=EC=9E=84=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 3 ++- src/test/java/wooteco/subway/domain/PathTest.java | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index 47f4a98e8..b605406ce 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -26,7 +26,8 @@ public int calculateFare() { if (distance <= FIRST_ADDITIONAL_FARE_DISTANCE) { return DEFAULT_FARE + calculateOverFare(distance - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, FIRST_ADDITIONAL_UNIT_FARE); } - return 0; + return DEFAULT_FARE + calculateOverFare(FIRST_ADDITIONAL_FARE_DISTANCE - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, FIRST_ADDITIONAL_UNIT_FARE) + + calculateOverFare(distance - FIRST_ADDITIONAL_FARE_DISTANCE, 8, FIRST_ADDITIONAL_UNIT_FARE); } private int calculateOverFare(int distance, int unitDistance, int overFare) { diff --git a/src/test/java/wooteco/subway/domain/PathTest.java b/src/test/java/wooteco/subway/domain/PathTest.java index 8e25ae0f8..8a0f03ce6 100644 --- a/src/test/java/wooteco/subway/domain/PathTest.java +++ b/src/test/java/wooteco/subway/domain/PathTest.java @@ -25,4 +25,12 @@ void calculate50Fare() { assertThat(path.calculateFare()).isEqualTo(2050); } + + @Test + @DisplayName("50km 초과일 때 8km 마다 100원 추가된다.") + void calculate58Fare() { + Path path = new Path(stations, 58); + + assertThat(path.calculateFare()).isEqualTo(2150); + } } From 7f931b2a9eae8367b713b953fca7646687e526e3 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:52:24 +0900 Subject: [PATCH 15/28] =?UTF-8?q?fix:=20id=20=EA=B0=92=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/wooteco/subway/service/PathServiceTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/wooteco/subway/service/PathServiceTest.java b/src/test/java/wooteco/subway/service/PathServiceTest.java index 8219462c9..9333b548f 100644 --- a/src/test/java/wooteco/subway/service/PathServiceTest.java +++ b/src/test/java/wooteco/subway/service/PathServiceTest.java @@ -43,16 +43,16 @@ void findPath() { sectionDao.save(new Section(2L, station4, station3, 3)); // when - PathFindResponse path = pathService.findPath(new PathFindRequest(1, 3, 15)); + PathFindResponse path = pathService.findPath(new PathFindRequest(station1.getId(), station3.getId(), 15)); // then assertAll( () -> assertThat(path.getStations()) .extracting("id", "name") .containsExactly( - tuple(1L, "오리"), - tuple(2L, "배카라"), - tuple(3L, "오카라") + tuple(station1.getId(), station1.getName()), + tuple(station2.getId(), station2.getName()), + tuple(station3.getId(), station3.getName()) ), () -> assertThat(path.getDistance()).isEqualTo(4) ); From 6a0fa966103120b7d11f5371fd94b97664e4c2d0 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:52:43 +0900 Subject: [PATCH 16/28] =?UTF-8?q?feat:=20path=20=EC=9A=B4=EC=9E=84=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 12 ++++++++---- src/main/java/wooteco/subway/domain/Sections.java | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index b605406ce..c6330b3ec 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -7,8 +7,9 @@ public class Path { private static final int DEFAULT_FARE = 1250; private static final int DEFAULT_FARE_DISTANCE = 10; - private static final int FIRST_ADDITIONAL_UNIT_FARE = 100; + private static final int ADDITIONAL_UNIT_FARE = 100; private static final int FIRST_ADDITIONAL_UNIT_DISTANCE = 5; + private static final int SECOND_ADDITIONAL_UNIT_DISTANCE= 8; private static final int FIRST_ADDITIONAL_FARE_DISTANCE = 50; private final List stations; @@ -24,10 +25,13 @@ public int calculateFare() { return DEFAULT_FARE; } if (distance <= FIRST_ADDITIONAL_FARE_DISTANCE) { - return DEFAULT_FARE + calculateOverFare(distance - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, FIRST_ADDITIONAL_UNIT_FARE); + return DEFAULT_FARE + calculateOverFare(distance - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, + ADDITIONAL_UNIT_FARE); } - return DEFAULT_FARE + calculateOverFare(FIRST_ADDITIONAL_FARE_DISTANCE - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, FIRST_ADDITIONAL_UNIT_FARE) - + calculateOverFare(distance - FIRST_ADDITIONAL_FARE_DISTANCE, 8, FIRST_ADDITIONAL_UNIT_FARE); + return DEFAULT_FARE + calculateOverFare(FIRST_ADDITIONAL_FARE_DISTANCE - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, + ADDITIONAL_UNIT_FARE) + + calculateOverFare(distance - FIRST_ADDITIONAL_FARE_DISTANCE, SECOND_ADDITIONAL_UNIT_DISTANCE, + ADDITIONAL_UNIT_FARE); } private int calculateOverFare(int distance, int unitDistance, int overFare) { diff --git a/src/main/java/wooteco/subway/domain/Sections.java b/src/main/java/wooteco/subway/domain/Sections.java index 04df2abd8..fbd116ddf 100644 --- a/src/main/java/wooteco/subway/domain/Sections.java +++ b/src/main/java/wooteco/subway/domain/Sections.java @@ -186,7 +186,7 @@ private Section upperSection(final Section section) { } public void checkExistStations(final Station source, final Station target) { - if (isNotExistStation(source) || isNotExistStation(target)) { + if (isNotExistStation(source) && isNotExistStation(target)) { throw new IllegalStateException("현재 Sections에 존재하지 않는 station입니다."); } } From 77c07b408302513998e727ebd0fb87fbe2c2c9f0 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:55:10 +0900 Subject: [PATCH 17/28] =?UTF-8?q?feat:=20path=20=EC=A1=B0=ED=9A=8C=20api?= =?UTF-8?q?=20=EC=9A=94=EA=B8=88=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/dto/path/PathFindResponse.java | 2 +- .../java/wooteco/subway/acceptance/PathFindAcceptanceTest.java | 2 +- src/test/java/wooteco/subway/service/PathServiceTest.java | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/wooteco/subway/dto/path/PathFindResponse.java b/src/main/java/wooteco/subway/dto/path/PathFindResponse.java index 0b9646bdc..261527bb6 100644 --- a/src/main/java/wooteco/subway/dto/path/PathFindResponse.java +++ b/src/main/java/wooteco/subway/dto/path/PathFindResponse.java @@ -25,7 +25,7 @@ public static PathFindResponse from(Path path) { .stream() .map(StationResponse::from) .collect(Collectors.toList()); - return new PathFindResponse(stationResponses, path.getDistance(), 0); + return new PathFindResponse(stationResponses, path.getDistance(), path.calculateFare()); } public List getStations() { diff --git a/src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java index c4f661dd5..697a195d7 100644 --- a/src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/PathFindAcceptanceTest.java @@ -43,7 +43,7 @@ void findPath() { Long stationId3 = postStations(new StationSaveRequest("선릉역")) .as(StationResponse.class) .getId(); - Long lineId = postLines(new LineSaveRequest("신분당선", "bg-red-600", stationId1, stationId3, 10)) + Long lineId = postLines(new LineSaveRequest("신분당선", "bg-red-600", stationId1, stationId3, 9)) .as(LineResponse.class) .getId(); postSections(lineId, new SectionSaveRequest(stationId2, stationId3, 6)); diff --git a/src/test/java/wooteco/subway/service/PathServiceTest.java b/src/test/java/wooteco/subway/service/PathServiceTest.java index 9333b548f..e7f10a184 100644 --- a/src/test/java/wooteco/subway/service/PathServiceTest.java +++ b/src/test/java/wooteco/subway/service/PathServiceTest.java @@ -54,7 +54,8 @@ void findPath() { tuple(station2.getId(), station2.getName()), tuple(station3.getId(), station3.getName()) ), - () -> assertThat(path.getDistance()).isEqualTo(4) + () -> assertThat(path.getDistance()).isEqualTo(4), + () -> assertThat(path.getFare()).isEqualTo(1250) ); } } From c03af538a6f5be933cbf2b82b8e376d24ef6cbe2 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 16:59:08 +0900 Subject: [PATCH 18/28] =?UTF-8?q?refactor:=20Path=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index c6330b3ec..b49dbb89a 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -9,7 +9,7 @@ public class Path { private static final int ADDITIONAL_UNIT_FARE = 100; private static final int FIRST_ADDITIONAL_UNIT_DISTANCE = 5; - private static final int SECOND_ADDITIONAL_UNIT_DISTANCE= 8; + private static final int OVER_ADDITIONAL_UNIT_DISTANCE = 8; private static final int FIRST_ADDITIONAL_FARE_DISTANCE = 50; private final List stations; @@ -25,19 +25,30 @@ public int calculateFare() { return DEFAULT_FARE; } if (distance <= FIRST_ADDITIONAL_FARE_DISTANCE) { - return DEFAULT_FARE + calculateOverFare(distance - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, - ADDITIONAL_UNIT_FARE); + return DEFAULT_FARE + calculateFirstAdditionalFare(); } - return DEFAULT_FARE + calculateOverFare(FIRST_ADDITIONAL_FARE_DISTANCE - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, - ADDITIONAL_UNIT_FARE) - + calculateOverFare(distance - FIRST_ADDITIONAL_FARE_DISTANCE, SECOND_ADDITIONAL_UNIT_DISTANCE, - ADDITIONAL_UNIT_FARE); + return DEFAULT_FARE + calculateFirstAdditionalMaxFare() + calculateOverAdditionalFare(); } private int calculateOverFare(int distance, int unitDistance, int overFare) { return (int) ((Math.ceil((distance - 1) / unitDistance) + 1) * overFare); } + private int calculateFirstAdditionalFare() { + return calculateOverFare(distance - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, + ADDITIONAL_UNIT_FARE); + } + + private int calculateFirstAdditionalMaxFare() { + return calculateOverFare(FIRST_ADDITIONAL_FARE_DISTANCE - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, + ADDITIONAL_UNIT_FARE); + } + + private int calculateOverAdditionalFare() { + return calculateOverFare(distance - FIRST_ADDITIONAL_FARE_DISTANCE, OVER_ADDITIONAL_UNIT_DISTANCE, + ADDITIONAL_UNIT_FARE); + } + public List getStations() { return stations; } From 2273bf59893f0706436504a988d23f6022fe0eee Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 17:30:34 +0900 Subject: [PATCH 19/28] =?UTF-8?q?fix:=20Vertex=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/strategy/FindDijkstraShortestPathStrategy.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java index 302bebad8..c73e126d4 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -16,15 +16,15 @@ public class FindDijkstraShortestPathStrategy implements FindPathStrategy { public Path findPath(final Station source, final Station target, final Sections sections) { sections.checkExistStations(source, target); WeightedMultigraph graph = new WeightedMultigraph<>(DefaultWeightedEdge.class); - addVertextStation(sections, graph); + addVertexStation(sections, graph); addEdgeWeightStation(sections, graph); GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); } - private void addVertextStation(final Sections sections, - final WeightedMultigraph graph) { + private void addVertexStation(final Sections sections, + final WeightedMultigraph graph) { List allStations = sections.getAllStations(); for (Station station : allStations) { graph.addVertex(station); From e1461bc29555517668253102e1d750d9662b752e Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 17:53:38 +0900 Subject: [PATCH 20/28] =?UTF-8?q?fix:=20=EA=B2=BD=EB=A1=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=B0=BE=EC=A7=80=20=EB=AA=BB=ED=96=88=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FindDijkstraShortestPathStrategy.java | 10 +++++++-- .../FindDijkstraShortestPathStrategyTest.java | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java index c73e126d4..b1f05415e 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -9,6 +9,7 @@ import wooteco.subway.domain.Section; import wooteco.subway.domain.Sections; import wooteco.subway.domain.Station; +import wooteco.subway.exception.NotFoundException; public class FindDijkstraShortestPathStrategy implements FindPathStrategy { @@ -19,8 +20,13 @@ public Path findPath(final Station source, final Station target, final Sections addVertexStation(sections, graph); addEdgeWeightStation(sections, graph); - GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); - return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); + try { + GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); + List vertexList = shortestPath.getVertexList(); + return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); + } catch (NullPointerException exception) { + throw new NotFoundException("갈 수 있는 경로를 찾을 수 없습니다."); + } } private void addVertexStation(final Sections sections, diff --git a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java index 2c6e541d9..8ef94c49a 100644 --- a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java +++ b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java @@ -11,6 +11,7 @@ import wooteco.subway.domain.Section; import wooteco.subway.domain.Sections; import wooteco.subway.domain.Station; +import wooteco.subway.exception.NotFoundException; class FindDijkstraShortestPathStrategyTest { @@ -57,4 +58,24 @@ void findPath() { () -> assertThat(path.getDistance()).isEqualTo(4) ); } + + @Test + @DisplayName("경로가 없는 경우 예외가 발생한다.") + void findPathExceptionByNotFoundPath() { + // given + Station station1 = new Station(1L, "오리"); + Station station2 = new Station(2L, "배카라"); + Station station3 = new Station(3L, "오카라"); + Station station4 = new Station(4L, "레넌"); + + Sections sections = new Sections( + List.of( + new Section(1L, 1L, station1, station2, 2), + new Section(2L, 2L, station3, station4, 3))); + + FindPathStrategy findPathStrategy = new FindDijkstraShortestPathStrategy(); + assertThatThrownBy(() -> findPathStrategy.findPath(station1, station4, sections)) + .isInstanceOf(NotFoundException.class) + .hasMessage("갈 수 있는 경로를 찾을 수 없습니다."); + } } From fdff67aa2250fa9017773c851af913de06ba20d2 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 17:54:17 +0900 Subject: [PATCH 21/28] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=EA=B5=AC=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/strategy/FindDijkstraShortestPathStrategy.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java index b1f05415e..3ffd7ea0d 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -22,7 +22,6 @@ public Path findPath(final Station source, final Station target, final Sections try { GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); - List vertexList = shortestPath.getVertexList(); return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); } catch (NullPointerException exception) { throw new NotFoundException("갈 수 있는 경로를 찾을 수 없습니다."); From f3085e7c31f251a4c3442c152f2d4ee5569b8152 Mon Sep 17 00:00:00 2001 From: jinyoungchoi95 Date: Tue, 17 May 2022 18:01:32 +0900 Subject: [PATCH 22/28] =?UTF-8?q?refactor:=20getPath=20null=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=A9=EB=B2=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategy/FindDijkstraShortestPathStrategy.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java index 3ffd7ea0d..9ee029692 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -1,6 +1,7 @@ package wooteco.subway.domain.strategy; import java.util.List; +import java.util.Optional; import org.jgrapht.GraphPath; import org.jgrapht.alg.shortestpath.DijkstraShortestPath; import org.jgrapht.graph.DefaultWeightedEdge; @@ -20,12 +21,10 @@ public Path findPath(final Station source, final Station target, final Sections addVertexStation(sections, graph); addEdgeWeightStation(sections, graph); - try { - GraphPath shortestPath = new DijkstraShortestPath(graph).getPath(source, target); - return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); - } catch (NullPointerException exception) { - throw new NotFoundException("갈 수 있는 경로를 찾을 수 없습니다."); - } + DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); + GraphPath shortestPath = Optional.ofNullable(dijkstraShortestPath.getPath(source, target)) + .orElseThrow(() -> new NotFoundException("갈 수 있는 경로를 찾을 수 없습니다.")); + return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); } private void addVertexStation(final Sections sections, From bf08e3471b0256d1191505dd46a6e6288512b37a Mon Sep 17 00:00:00 2001 From: brorae Date: Tue, 17 May 2022 22:56:35 +0900 Subject: [PATCH 23/28] =?UTF-8?q?refactor:=20=EB=A1=9C=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=A7=80=EC=A0=95=20=EB=B0=8F=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategy/FindDijkstraShortestPathStrategy.java | 9 ++++++--- .../wooteco/subway/domain/strategy/FindPathStrategy.java | 1 - src/test/java/wooteco/subway/domain/PathTest.java | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java index 9ee029692..29dfa587c 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategy.java @@ -17,13 +17,16 @@ public class FindDijkstraShortestPathStrategy implements FindPathStrategy { @Override public Path findPath(final Station source, final Station target, final Sections sections) { sections.checkExistStations(source, target); + WeightedMultigraph graph = new WeightedMultigraph<>(DefaultWeightedEdge.class); addVertexStation(sections, graph); addEdgeWeightStation(sections, graph); - DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); - GraphPath shortestPath = Optional.ofNullable(dijkstraShortestPath.getPath(source, target)) - .orElseThrow(() -> new NotFoundException("갈 수 있는 경로를 찾을 수 없습니다.")); + DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath<>(graph); + GraphPath shortestPath = + Optional.ofNullable(dijkstraShortestPath.getPath(source, target)) + .orElseThrow(() -> new NotFoundException("갈 수 있는 경로를 찾을 수 없습니다.")); + return new Path(shortestPath.getVertexList(), (int) shortestPath.getWeight()); } diff --git a/src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java b/src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java index 6ecef682a..f84da3ea4 100644 --- a/src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java +++ b/src/main/java/wooteco/subway/domain/strategy/FindPathStrategy.java @@ -7,5 +7,4 @@ public interface FindPathStrategy { Path findPath(Station source, Station target, Sections sections); - } diff --git a/src/test/java/wooteco/subway/domain/PathTest.java b/src/test/java/wooteco/subway/domain/PathTest.java index 8a0f03ce6..47af762ac 100644 --- a/src/test/java/wooteco/subway/domain/PathTest.java +++ b/src/test/java/wooteco/subway/domain/PathTest.java @@ -12,7 +12,7 @@ class PathTest { @Test @DisplayName("10km 이하일 때 기본 운임은 1250원이다.") - void calcualteDefaultFare() { + void calculateDefaultFare() { Path path = new Path(stations, 10); assertThat(path.calculateFare()).isEqualTo(1250); From 99121c2c77037b30f63de1a029c084b89cefa83b Mon Sep 17 00:00:00 2001 From: brorae Date: Tue, 17 May 2022 23:11:31 +0900 Subject: [PATCH 24/28] =?UTF-8?q?refactor:=20path=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20ParameterizedTest=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 14 ++++++ .../java/wooteco/subway/domain/PathTest.java | 46 ++++++++++++++----- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index b49dbb89a..18f3cddcc 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -16,10 +16,24 @@ public class Path { private final int distance; public Path(final List stations, final int distance) { + validateStations(stations); + validateDistance(distance); this.stations = stations; this.distance = distance; } + public void validateStations(final List stations) { + if(stations.isEmpty()) { + throw new IllegalArgumentException("경로는 비어서는 안됩니다."); + } + } + + public void validateDistance(final int distance) { + if (distance <= 0) { + throw new IllegalArgumentException("거리는 0보다 커야합니다."); + } + } + public int calculateFare() { if (distance <= DEFAULT_FARE_DISTANCE) { return DEFAULT_FARE; diff --git a/src/test/java/wooteco/subway/domain/PathTest.java b/src/test/java/wooteco/subway/domain/PathTest.java index 47af762ac..a0bc21e10 100644 --- a/src/test/java/wooteco/subway/domain/PathTest.java +++ b/src/test/java/wooteco/subway/domain/PathTest.java @@ -1,36 +1,60 @@ package wooteco.subway.domain; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; class PathTest { private final List stations = List.of(new Station(1L, "강남역"), new Station(2L, "선릉역")); @Test + @DisplayName("경로의 역들이 비어 있으면 예외가 발생한다.") + void constructExceptionByEmptyStations() { + assertThatThrownBy(() -> new Path(new ArrayList<>(), 10)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 0}) + @DisplayName("경로의 거리가 0보다 작거나 같으면 예외가 발생한다.") + void constructExceptionByLessThanZeroDistance(final int distance) { + assertThatThrownBy(() -> new Path(stations, distance)) + .isInstanceOf(IllegalArgumentException.class); + } + + + @ParameterizedTest + @CsvSource(value = {"1, 1250", "10, 1250"}) @DisplayName("10km 이하일 때 기본 운임은 1250원이다.") - void calculateDefaultFare() { - Path path = new Path(stations, 10); + void calculateDefaultFare(final int distance, final int expected) { + Path path = new Path(stations, distance); - assertThat(path.calculateFare()).isEqualTo(1250); + assertThat(path.calculateFare()).isEqualTo(expected); } - @Test + @ParameterizedTest + @CsvSource(value = {"20, 1450", "30, 1650", "50, 2050"}) @DisplayName("50km 이하일 때 5km 마다 100원 추가된다.") - void calculate50Fare() { - Path path = new Path(stations, 50); + void calculateFarLessThan50(final int distance, final int expected) { + Path path = new Path(stations, distance); - assertThat(path.calculateFare()).isEqualTo(2050); + assertThat(path.calculateFare()).isEqualTo(expected); } - @Test + @ParameterizedTest + @CsvSource(value = {"58, 2150", "90, 2550"}) @DisplayName("50km 초과일 때 8km 마다 100원 추가된다.") - void calculate58Fare() { - Path path = new Path(stations, 58); + void calculateMoreThan50(final int distance, final int expected) { + Path path = new Path(stations, distance); - assertThat(path.calculateFare()).isEqualTo(2150); + assertThat(path.calculateFare()).isEqualTo(expected); } } From ddfbe4b9b6d2b46e8a2d46d8dd497a24f18b57e9 Mon Sep 17 00:00:00 2001 From: brorae Date: Tue, 17 May 2022 23:23:02 +0900 Subject: [PATCH 25/28] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/domain/Path.java | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/main/java/wooteco/subway/domain/Path.java b/src/main/java/wooteco/subway/domain/Path.java index 18f3cddcc..796efc3ed 100644 --- a/src/main/java/wooteco/subway/domain/Path.java +++ b/src/main/java/wooteco/subway/domain/Path.java @@ -5,62 +5,63 @@ public class Path { private static final int DEFAULT_FARE = 1250; - private static final int DEFAULT_FARE_DISTANCE = 10; - - private static final int ADDITIONAL_UNIT_FARE = 100; - private static final int FIRST_ADDITIONAL_UNIT_DISTANCE = 5; - private static final int OVER_ADDITIONAL_UNIT_DISTANCE = 8; - private static final int FIRST_ADDITIONAL_FARE_DISTANCE = 50; + private static final int UNIT_OF_ADDITIONAL_FARE = 100; + + private static final int DISTANCE_OF_DEFAULT_FARE = 10; + private static final int DISTANCE_OF_FIRST_ADDITIONAL_UNIT = 5; + private static final int DISTANCE_OF_OVER_ADDITIONAL_UNIT = 8; + private static final int DISTANCE_OF_OVER_ADDITIONAL_FARE = 50; private final List stations; private final int distance; public Path(final List stations, final int distance) { - validateStations(stations); - validateDistance(distance); + validateEmptyStations(stations); + validatePositiveDistance(distance); this.stations = stations; this.distance = distance; } - public void validateStations(final List stations) { + public void validateEmptyStations(final List stations) { if(stations.isEmpty()) { throw new IllegalArgumentException("경로는 비어서는 안됩니다."); } } - public void validateDistance(final int distance) { + public void validatePositiveDistance(final int distance) { if (distance <= 0) { throw new IllegalArgumentException("거리는 0보다 커야합니다."); } } public int calculateFare() { - if (distance <= DEFAULT_FARE_DISTANCE) { + if (distance <= DISTANCE_OF_DEFAULT_FARE) { return DEFAULT_FARE; } - if (distance <= FIRST_ADDITIONAL_FARE_DISTANCE) { + if (distance <= DISTANCE_OF_OVER_ADDITIONAL_FARE) { return DEFAULT_FARE + calculateFirstAdditionalFare(); } return DEFAULT_FARE + calculateFirstAdditionalMaxFare() + calculateOverAdditionalFare(); } - private int calculateOverFare(int distance, int unitDistance, int overFare) { - return (int) ((Math.ceil((distance - 1) / unitDistance) + 1) * overFare); + private int calculateFirstAdditionalFare() { + return calculateOverFare(distance - DISTANCE_OF_DEFAULT_FARE, DISTANCE_OF_FIRST_ADDITIONAL_UNIT + ); } - private int calculateFirstAdditionalFare() { - return calculateOverFare(distance - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, - ADDITIONAL_UNIT_FARE); + private int calculateOverFare(int distance, int unitDistance) { + return (int) ((Math.ceil((distance - 1) / unitDistance) + 1) * UNIT_OF_ADDITIONAL_FARE); } private int calculateFirstAdditionalMaxFare() { - return calculateOverFare(FIRST_ADDITIONAL_FARE_DISTANCE - DEFAULT_FARE_DISTANCE, FIRST_ADDITIONAL_UNIT_DISTANCE, - ADDITIONAL_UNIT_FARE); + return calculateOverFare(DISTANCE_OF_OVER_ADDITIONAL_FARE - DISTANCE_OF_DEFAULT_FARE, + DISTANCE_OF_FIRST_ADDITIONAL_UNIT + ); } private int calculateOverAdditionalFare() { - return calculateOverFare(distance - FIRST_ADDITIONAL_FARE_DISTANCE, OVER_ADDITIONAL_UNIT_DISTANCE, - ADDITIONAL_UNIT_FARE); + return calculateOverFare(distance - DISTANCE_OF_OVER_ADDITIONAL_FARE, DISTANCE_OF_OVER_ADDITIONAL_UNIT + ); } public List getStations() { From c139f08a98a22fbfbd149d617e77d9ad7dd028c4 Mon Sep 17 00:00:00 2001 From: brorae Date: Tue, 17 May 2022 23:53:15 +0900 Subject: [PATCH 26/28] =?UTF-8?q?refactor:=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/wooteco/subway/domain/PathTest.java | 4 ++-- .../domain/strategy/FindDijkstraShortestPathStrategyTest.java | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/wooteco/subway/domain/PathTest.java b/src/test/java/wooteco/subway/domain/PathTest.java index a0bc21e10..44a4124d2 100644 --- a/src/test/java/wooteco/subway/domain/PathTest.java +++ b/src/test/java/wooteco/subway/domain/PathTest.java @@ -41,7 +41,7 @@ void calculateDefaultFare(final int distance, final int expected) { } @ParameterizedTest - @CsvSource(value = {"20, 1450", "30, 1650", "50, 2050"}) + @CsvSource(value = {"16, 1450", "20, 1450", "30, 1650", "50, 2050"}) @DisplayName("50km 이하일 때 5km 마다 100원 추가된다.") void calculateFarLessThan50(final int distance, final int expected) { Path path = new Path(stations, distance); @@ -50,7 +50,7 @@ void calculateFarLessThan50(final int distance, final int expected) { } @ParameterizedTest - @CsvSource(value = {"58, 2150", "90, 2550"}) + @CsvSource(value = {"58, 2150", "60, 2250", "90, 2550"}) @DisplayName("50km 초과일 때 8km 마다 100원 추가된다.") void calculateMoreThan50(final int distance, final int expected) { Path path = new Path(stations, distance); diff --git a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java index 8ef94c49a..330a04e12 100644 --- a/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java +++ b/src/test/java/wooteco/subway/domain/strategy/FindDijkstraShortestPathStrategyTest.java @@ -50,9 +50,11 @@ void findPath() { new Section(3L, 2L, station1, station4, 3), new Section(4L, 2L, station4, station3, 3))); + //when FindPathStrategy findPathStrategy = new FindDijkstraShortestPathStrategy(); Path path = findPathStrategy.findPath(station1, station3, sections); + //then assertAll( () -> assertThat(path.getStations()).containsExactly(station1, station2, station3), () -> assertThat(path.getDistance()).isEqualTo(4) @@ -73,6 +75,7 @@ void findPathExceptionByNotFoundPath() { new Section(1L, 1L, station1, station2, 2), new Section(2L, 2L, station3, station4, 3))); + //when & then FindPathStrategy findPathStrategy = new FindDijkstraShortestPathStrategy(); assertThatThrownBy(() -> findPathStrategy.findPath(station1, station4, sections)) .isInstanceOf(NotFoundException.class) From d8b6a360d4df46fc02667f111aadcfe706d431ca Mon Sep 17 00:00:00 2001 From: brorae Date: Wed, 18 May 2022 00:04:03 +0900 Subject: [PATCH 27/28] =?UTF-8?q?refactor:=20request=20validation=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wooteco/subway/dto/path/PathFindRequest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/wooteco/subway/dto/path/PathFindRequest.java b/src/main/java/wooteco/subway/dto/path/PathFindRequest.java index dc6c95cc9..823806fd8 100644 --- a/src/main/java/wooteco/subway/dto/path/PathFindRequest.java +++ b/src/main/java/wooteco/subway/dto/path/PathFindRequest.java @@ -1,9 +1,16 @@ package wooteco.subway.dto.path; +import javax.validation.constraints.Positive; + public class PathFindRequest { + @Positive(message = "출발지의 id는 양수 값만 들어올 수 있습니다.") private long source; + + @Positive(message = "도착지의 id는 양수 값만 들어올 수 있습니다.") private long target; + + @Positive(message = "나이는 양수 값만 들어올 수 있습니다.") private int age; private PathFindRequest() { From 034d09b1865577c28b43a83da70cb341410e3a00 Mon Sep 17 00:00:00 2001 From: brorae Date: Wed, 18 May 2022 00:21:30 +0900 Subject: [PATCH 28/28] =?UTF-8?q?refactor:=20=EB=85=B8=EC=84=A0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EA=B5=AC=EA=B0=84=EB=8F=84=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/dao/section/InmemorySectionDao.java | 12 ++++++++++++ .../wooteco/subway/dao/section/JdbcSectionDao.java | 6 ++++++ .../java/wooteco/subway/dao/section/SectionDao.java | 2 ++ .../java/wooteco/subway/service/LineService.java | 1 + .../subway/dao/section/InmemorySectionDaoTest.java | 11 +++++++++++ 5 files changed, 32 insertions(+) diff --git a/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java b/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java index 9ec6e0746..8ce29e78c 100644 --- a/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java +++ b/src/main/java/wooteco/subway/dao/section/InmemorySectionDao.java @@ -69,4 +69,16 @@ public int delete(final long sectionId) { sections.remove(sectionId); return 1; } + + @Override + public int deleteByLineId(long lineId) { + List removeIds = sections.values().stream() + .filter(section -> section.getLineId() == lineId) + .map(section -> section.getId()) + .collect(Collectors.toList()); + for (Long id : removeIds) { + sections.remove(id); + } + return 1; + } } diff --git a/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java b/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java index a2b091ab8..247c84b4b 100644 --- a/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java +++ b/src/main/java/wooteco/subway/dao/section/JdbcSectionDao.java @@ -93,4 +93,10 @@ public int delete(final long sectionId) { final String sql = "delete from SECTION where id = ?"; return jdbcTemplate.update(sql, sectionId); } + + @Override + public int deleteByLineId(final long lineId) { + final String sql = "delete from SECTION where line_id = ?"; + return jdbcTemplate.update(sql, lineId); + } } diff --git a/src/main/java/wooteco/subway/dao/section/SectionDao.java b/src/main/java/wooteco/subway/dao/section/SectionDao.java index 0ac5284d7..a66e67a26 100644 --- a/src/main/java/wooteco/subway/dao/section/SectionDao.java +++ b/src/main/java/wooteco/subway/dao/section/SectionDao.java @@ -14,4 +14,6 @@ public interface SectionDao { int updateSections(List
sections); int delete(long sectionId); + + int deleteByLineId(long lineId); } diff --git a/src/main/java/wooteco/subway/service/LineService.java b/src/main/java/wooteco/subway/service/LineService.java index 442d12eaa..9546e837e 100644 --- a/src/main/java/wooteco/subway/service/LineService.java +++ b/src/main/java/wooteco/subway/service/LineService.java @@ -72,6 +72,7 @@ public void update(final long lineId, final LineUpdateRequest request) { public void delete(final Long lineId) { checkExistLine(lineId); lineDao.delete(lineId); + sectionDao.deleteByLineId(lineId); } private void checkExistLine(final Long lineId) { diff --git a/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java b/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java index 9db8246d1..55639c6ad 100644 --- a/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java +++ b/src/test/java/wooteco/subway/dao/section/InmemorySectionDaoTest.java @@ -77,4 +77,15 @@ void delete() { assertThat(sectionDao.delete(sectionId)).isEqualTo(1); } + + @Test + @DisplayName("Section을 line별로 삭제할 수 있다.") + void deleteByLineId() { + long lineId = lineDao.save(new Line("신분당선", "bg-red-600")); + long stationId1 = stationDao.save(new Station("오리")); + long stationId2 = stationDao.save(new Station("레넌")); + sectionDao.save(new Section(lineId, stationDao.findById(stationId1), stationDao.findById(stationId2), 10)); + + assertThat(sectionDao.deleteByLineId(lineId)).isEqualTo(1); + } }