Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 가까운 지하철 역을 2개 보여준다. #575

Open
wants to merge 8 commits into
base: dev-be
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public enum ExceptionCode {
ARTICLE_NOT_FOUND(HttpStatus.BAD_REQUEST, "해당 아티클이 존재하지 않습니다."),

// Station
STATION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "지하철 역을 찾을 수 없습니다.");
STATION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "지하철 역을 찾을 수 없습니다."),
STATION_NAME_NOT_SAME(HttpStatus.INTERNAL_SERVER_ERROR, "지하철 역을 찾을 수 없습니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class SubwayStationController {
Expand All @@ -17,10 +18,10 @@ public SubwayStationController(SubwayStationService subwayStationService) {
}

@GetMapping("/stations/nearest")
public ResponseEntity<SubwayStationResponse> readNearestStation(@RequestParam("latitude") double latitude,
@RequestParam("longitude") double longitude) {
public ResponseEntity<List<SubwayStationResponse>> readNearestStation(@RequestParam("latitude") Double latitude,
@RequestParam("longitude") Double longitude) {

SubwayStationResponse response = subwayStationService.readNearestStation(latitude, longitude);
List<SubwayStationResponse> response = subwayStationService.readNearestStation(latitude, longitude);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public class SubwayStation {

private static final int METER_PER_DEGREE = 111_320;

private final Integer id;
private final String name;
private final String line;
Expand All @@ -16,6 +18,13 @@ public SubwayStation(Integer id, String name, String line, double latitude, doub
this.longitude = longitude;
}

public double calculateDistance(double latitude, double longitude) {
double dx = (this.latitude - latitude) * METER_PER_DEGREE;
double dy =
(this.longitude - longitude) * METER_PER_DEGREE * Math.cos(this.latitude);
return Math.sqrt(dx * dx + dy * dy);
}

public Integer getId() {
return id;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
package com.bang_ggood.station.dto;

import com.bang_ggood.global.exception.BangggoodException;
import com.bang_ggood.global.exception.ExceptionCode;
import com.bang_ggood.station.domain.SubwayStation;
import java.util.ArrayList;
import java.util.List;

public record SubwayStationResponse(String stationName, String stationLine, Integer walkingTime) {
public class SubwayStationResponse {

private final String stationName;
private final List<String> stationLine;
private Integer walkingTime;

public SubwayStationResponse(String stationName, List<String> stationLine, Integer walkingTime) {
this.stationName = stationName;
this.stationLine = stationLine;
this.walkingTime = walkingTime;
}

public static SubwayStationResponse of(SubwayStation station, Integer walkingTime) {
return new SubwayStationResponse(station.getName(), station.getLine(), walkingTime);
List<String> stationLine = new ArrayList<>();
stationLine.add(station.getLine());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어차피 1개만 받는 것 같은데 왜 List를 선택하셨나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge() 메서드를 통해 다른 Response와 합치기 위함입니다.

잠실역 2호선.merge(잠실역 8호선) = 잠실역 2호선,8호선

return new SubwayStationResponse(station.getName(), stationLine, walkingTime);
}

public SubwayStationResponse merge(SubwayStationResponse response) {
if (!stationName.equals(response.stationName)) {
throw new BangggoodException(ExceptionCode.STATION_NAME_NOT_SAME);
}

stationLine.addAll(response.stationLine);
walkingTime = Math.min(walkingTime, response.walkingTime);
return this;
}

public String getStationName() {
return stationName;
}

public List<String> getStationLine() {
return stationLine;
}

public Integer getWalkingTime() {
return walkingTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,36 @@
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class SubwayStationService {

private static final int METER_PER_DEGREE = 111_320;
private static final double AVERAGE_WALKING_SPEED = 1.3 * 60; // meter per minute
// meter per second * minute unit * decreasing speed on open street
private static final double AVERAGE_WALKING_SPEED = 1.3 * 60 * 0.4;
private static final int MAX_NESTING_STATION_NUMBER = 4;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중첩된 지하철역 수가 4인 이유가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대한민국 내 환승역 최대 중첩수가 4 입니다!

private static final int REQUESTED_STATION_NUMBER = 2;
private static final List<SubwayStation> SUBWAY_STATIONS = SubwayReader.readSubwayStationData();

public SubwayStationResponse readNearestStation(double latitude, double longitude) {
return SUBWAY_STATIONS.stream()
public List<SubwayStationResponse> readNearestStation(double latitude, double longitude) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스트림이 길어서 가독성이 좀 떨어진다고 생각하는데, 인지 단위로 한번씩 끊는 것은 어떤가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다!

Map<String, Optional<SubwayStationResponse>> responseMap = SUBWAY_STATIONS.stream()
.map(station -> {
double dx = (station.getLatitude() - latitude) * METER_PER_DEGREE;
double dy =
(station.getLongitude() - longitude) * METER_PER_DEGREE * Math.cos(station.getLatitude());
double distance = Math.sqrt(dx * dx + dy * dy);
double distance = station.calculateDistance(latitude, longitude);
return SubwayStationResponse.of(station, (int) Math.round(distance / AVERAGE_WALKING_SPEED));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

거리 계산 로직을 메서드 분리하면 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다!

Comment on lines +27 to 28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SubwayStation 클래스에 walkingTime 계산을 요청할 수도 있지 않을까요?
뒤에 (int) Math.round(distance / AVERAGE_WALKING_SPEED)); 로직과 따로 있지 않아도 될 것 같아요

})
.min(Comparator.comparing(SubwayStationResponse::walkingTime))
.orElseThrow(() -> new BangggoodException(ExceptionCode.STATION_NOT_FOUND));
.sorted(Comparator.comparing(SubwayStationResponse::getWalkingTime))
.limit(MAX_NESTING_STATION_NUMBER * REQUESTED_STATION_NUMBER)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 어떤 의미의 코드인가요??

.collect(Collectors.groupingBy(
SubwayStationResponse::getStationName,
Collectors.reducing(SubwayStationResponse::merge)
));
Comment on lines +32 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분만 조금 더 개선하면 이해하기 쉬울 것 같아요!


return responseMap.values().stream()
.map(optional -> optional.orElseThrow(() -> new BangggoodException(ExceptionCode.STATION_NOT_FOUND)))
.sorted(Comparator.comparing(SubwayStationResponse::getWalkingTime))
.limit(REQUESTED_STATION_NUMBER)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,66 @@
package com.bang_ggood.station;

import com.bang_ggood.IntegrationTestSupport;
import com.bang_ggood.station.dto.SubwayStationResponse;
import com.bang_ggood.station.service.SubwayStationService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

public class SubwayStationServiceTest extends IntegrationTestSupport {

@Autowired
SubwayStationService subwayStationService;

@DisplayName("가까운 지하철 2개 조회 성공")
@ParameterizedTest
@MethodSource("provideStationData")
void readNearestStation(double latitude, double longitude, Station nearestStation, Station nextNearestStation) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 코드를 맨 위로 올리는 것은 어떤가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다!

// given & when
List<SubwayStationResponse> responses = subwayStationService.readNearestStation(latitude, longitude);
assertThat(responses).hasSize(2);
SubwayStationResponse nearest = responses.get(0);
SubwayStationResponse nextNearest = responses.get(1);

// then
assertAll(() -> {
assertThat(nearest.getStationName()).isEqualTo(nearestStation.name);
assertThat(nearest.getStationLine()).containsAll(nearestStation.lines);
assertThat(nextNearest.getStationName()).isEqualTo(nextNearestStation.name);
assertThat(nextNearest.getStationLine()).containsAll(nextNearestStation.lines);
}
);
}

// check data in "https://apis.map.kakao.com/web/sample/addMapClickEventWithMarker/"
private static Stream<Arguments> provideStationData() {
return Stream.of(
Arguments.of(37.50495731889611, 126.7550884277559, "상동"),
Arguments.of(37.48352733443973, 126.80085909322227, "소사"),
Arguments.of(37.47909015564278, 126.9517354974442, "서울대입구(관악구청)"),
Arguments.of(37.516248619935034, 127.10303565244502, "잠실(송파구청)")
Arguments.of(37.517406150696104, 127.10333134512422,
new Station("잠실(송파구청)", List.of("2호선", "8호선")),
new Station("잠실나루", List.of("2호선"))),
Arguments.of(37.50808977595056, 127.04649615866747,
new Station("선정릉", List.of("9호선", "분당선")),
new Station("선릉", List.of("2호선", "분당선"))),
Arguments.of(37.538999998345446, 126.97201837726666,
new Station("남영", List.of("1호선")),
new Station("삼각지", List.of("4호선", "6호선")))
);
}

@DisplayName("가까운 지하철 조회 성공")
@ParameterizedTest
@MethodSource("provideStationData")
void readNearestStation(double latitude, double longitude, String nearestStationName) {
// given & when
String stationName = subwayStationService.readNearestStation(latitude, longitude).stationName();
private static class Station {
String name;
List<String> lines;

// then
assertThat(stationName).isEqualTo(nearestStationName);
public Station(String name, List<String> lines) {
this.name = name;
this.lines = lines;
}
}
}
Loading