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

주변 수거함 목록 조회 기능 요청 파라미터 객체화 #107

Closed
wants to merge 6 commits into from
12 changes: 12 additions & 0 deletions src/main/java/contest/collectingbox/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
package contest.collectingbox.global.config;

import contest.collectingbox.global.config.web.PointArgumentResolver;
import contest.collectingbox.global.config.web.TagsArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.maxAge(3000);
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new PointArgumentResolver());
resolvers.add(new TagsArgumentResolver());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package contest.collectingbox.global.config.web;

import contest.collectingbox.global.exception.CollectingBoxException;
import contest.collectingbox.global.exception.ErrorCode;
import contest.collectingbox.global.utils.GeometryUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.coyote.BadRequestException;
import org.locationtech.jts.geom.Point;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class PointArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Point.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

if (request.getParameter("latitude") == null) {
throw new MissingServletRequestParameterException("latitude", "Point");
}
if (request.getParameter("longitude") == null) {
throw new MissingServletRequestParameterException("longitude", "Point");
}

double latitude;
double longitude;

try {
latitude = Double.parseDouble(request.getParameter("latitude"));
longitude = Double.parseDouble(request.getParameter("longitude"));
return GeometryUtil.toPoint(longitude, latitude);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid parameter value for latitude or longitude");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package contest.collectingbox.global.config.web;

import contest.collectingbox.module.collectingbox.domain.Tag;
import contest.collectingbox.module.collectingbox.domain.Tags;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import java.util.Arrays;
import java.util.List;

public class TagsArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Tags.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
if (request.getParameter("tags") == null) {
throw new MissingServletRequestParameterException("tags", "Tags");
}

try {
List<Tag> tags = Arrays.stream(request.getParameter("tags").split(","))
.map(Tag::valueOf)
.toList();
return new Tags(tags);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid parameter value for tags");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ public enum ErrorCode {
NOT_SELECTED_TAG(BAD_REQUEST, "수거함 태그는 반드시 한 개 이상 설정해야 합니다."),
INVALID_REVIEW_CONTENT(BAD_REQUEST, "올바르지 않는 리뷰 내용입니다."),
INVALID_BEAN(BAD_REQUEST, "유효하지 않은 데이터입니다."),
MISSING_REQUEST_PARAM(BAD_REQUEST, "필수 요청 파라미터가 존재하지 않습니다."),
MISSING_REQUEST_PARAM(BAD_REQUEST, "'%s' 타입의 '%s' 요청 파라미터가 존재하지 않습니다."),
MISMATCH_REQUEST_PARAM(BAD_REQUEST, "요청 파라미터가 유효하지 않습니다."),
ILLEGAL_ARGUMENT(BAD_REQUEST, "유효하지 않은 값입니다."),

// 404
NOT_FOUND_COLLECTING_BOX(NOT_FOUND, "해당 수거함이 존재하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,19 @@ public ApiResponse<Object> methodValidException(MethodArgumentNotValidException
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(BAD_REQUEST)
public ApiResponse<Object> handleMissingParams(MissingServletRequestParameterException e) {
return ApiResponse.error(MISSING_REQUEST_PARAM, MISSING_REQUEST_PARAM.getMessage());
return ApiResponse.error(MISSING_REQUEST_PARAM,
String.format(MISSING_REQUEST_PARAM.getMessage(), e.getParameterType(), e.getParameterName()));
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(BAD_REQUEST)
public ApiResponse<Object> handleMissingParams(MethodArgumentTypeMismatchException e) {
return ApiResponse.error(MISMATCH_REQUEST_PARAM, MISSING_REQUEST_PARAM.getMessage());
}

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(BAD_REQUEST)
public ApiResponse<Object> handleIllegalArgument(IllegalArgumentException e) {
return ApiResponse.error(ILLEGAL_ARGUMENT, e.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package contest.collectingbox.module.collectingbox.application;

import contest.collectingbox.global.exception.CollectingBoxException;
import contest.collectingbox.global.exception.ErrorCode;
import contest.collectingbox.global.utils.GeometryUtil;
import contest.collectingbox.module.collectingbox.domain.CollectingBox;
import contest.collectingbox.module.collectingbox.domain.CollectingBoxRepository;
import contest.collectingbox.module.collectingbox.domain.Tag;
import contest.collectingbox.module.collectingbox.domain.Tags;
import contest.collectingbox.module.collectingbox.dto.CollectingBoxDetailResponse;
import contest.collectingbox.module.collectingbox.dto.CollectingBoxResponse;
import contest.collectingbox.module.location.domain.DongInfo;
Expand All @@ -19,8 +17,6 @@
import java.util.List;
import java.util.stream.Collectors;

import static contest.collectingbox.global.exception.ErrorCode.NOT_FOUND_COLLECTING_BOX;

@Service
@RequiredArgsConstructor
public class CollectingBoxService {
Expand All @@ -32,16 +28,9 @@ public class CollectingBoxService {
private int radius;

@Transactional(readOnly = true)
public List<CollectingBoxResponse> findCollectingBoxesWithinArea(final Double latitude,
final Double longitude,
final List<Tag> tags) {
if (tags.isEmpty()) {
throw new CollectingBoxException(ErrorCode.NOT_SELECTED_TAG);
}

Point center = GeometryUtil.toPoint(longitude, latitude);

return collectingBoxRepository.findAllWithinArea(center, radius, tags)
public List<CollectingBoxResponse> findCollectingBoxesWithinArea(final Point center,
final Tags tags) {
return collectingBoxRepository.findAllWithinArea(center, radius, tags.toUnmodifiableList())
.stream()
.map(CollectingBoxResponse::fromEntity)
.collect(Collectors.toList());
Expand All @@ -53,25 +42,21 @@ public CollectingBoxDetailResponse findBoxDetailById(Long collectionId) {
}

@Transactional(readOnly = true)
public List<CollectingBoxResponse> searchCollectingBoxes(final String query, final List<Tag> tags) {
if (tags.isEmpty()) {
throw new CollectingBoxException(ErrorCode.NOT_SELECTED_TAG);
}

public List<CollectingBoxResponse> searchCollectingBoxes(final String query, final Tags tags) {
// '강남구'
if (query.endsWith("구")) {
return searchBySigunguNm(query, tags);
return searchBySigunguNm(query, tags.toUnmodifiableList());
}

String[] splitQuery = query.split(" ");

// '역삼1동'
if (splitQuery.length == 1) {
return searchByDongNm(query, tags);
return searchByDongNm(query, tags.toUnmodifiableList());
}

// '강남구 역삼1동'
return searchByDongNm(splitQuery[1], tags);
return searchByDongNm(splitQuery[1], tags.toUnmodifiableList());
}

private List<CollectingBoxResponse> searchBySigunguNm(String query, List<Tag> tags) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package contest.collectingbox.module.collectingbox.domain;

import contest.collectingbox.global.exception.CollectingBoxException;
import contest.collectingbox.global.exception.ErrorCode;

import java.util.Collections;
import java.util.List;

public class Tags {
private final List<Tag> tags;

public Tags(List<Tag> tags) {
if (tags == null || tags.isEmpty()) {
throw new CollectingBoxException(ErrorCode.NOT_SELECTED_TAG);
}
this.tags = tags;
}

public List<Tag> toUnmodifiableList() {
return Collections.unmodifiableList(tags);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import contest.collectingbox.global.common.ApiResponse;
import contest.collectingbox.module.collectingbox.application.CollectingBoxService;
import contest.collectingbox.module.collectingbox.domain.Tag;
import contest.collectingbox.module.collectingbox.domain.Tags;
import contest.collectingbox.module.collectingbox.dto.CollectingBoxDetailResponse;
import contest.collectingbox.module.collectingbox.dto.CollectingBoxResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import org.locationtech.jts.geom.Point;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -22,16 +22,15 @@ public class CollectingBoxController {

@Operation(summary = "수거함 목록 조회", description = "위도와 경도를 기준으로 200m 반경에 위치한 수거함 목록을 조회합니다.")
@GetMapping
public ApiResponse<List<CollectingBoxResponse>> findCollectingBoxesWithinArea(@RequestParam final Double latitude,
@RequestParam final Double longitude,
@RequestParam final List<Tag> tags) {
return ApiResponse.ok(collectingBoxService.findCollectingBoxesWithinArea(latitude, longitude, tags));
public ApiResponse<List<CollectingBoxResponse>> findCollectingBoxesWithinArea(final Point center,
final Tags tags) {
return ApiResponse.ok(collectingBoxService.findCollectingBoxesWithinArea(center, tags));
}

@Operation(summary = "지역별 수거함 검색", description = "구/동 단위로 검색한 주소에 위치한 수거함 목록을 조회합니다.")
@GetMapping("/search")
public ApiResponse<List<CollectingBoxResponse>> searchCollectingBoxes(@RequestParam final String query,
@RequestParam final List<Tag> tags) {
final Tags tags) {
return ApiResponse.ok(collectingBoxService.searchCollectingBoxes(query, tags));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import contest.collectingbox.global.utils.GeometryUtil;
import contest.collectingbox.module.collectingbox.domain.CollectingBox;
import contest.collectingbox.module.collectingbox.domain.CollectingBoxRepository;
import contest.collectingbox.module.collectingbox.domain.Tag;
import contest.collectingbox.module.collectingbox.domain.Tags;
import contest.collectingbox.module.collectingbox.dto.CollectingBoxDetailResponse;
import contest.collectingbox.module.collectingbox.dto.CollectingBoxResponse;
import contest.collectingbox.module.location.domain.Location;
Expand All @@ -23,7 +23,7 @@
import java.util.Collections;
import java.util.List;

import static contest.collectingbox.global.exception.ErrorCode.*;
import static contest.collectingbox.global.exception.ErrorCode.NOT_FOUND_COLLECTING_BOX;
import static contest.collectingbox.module.collectingbox.domain.Tag.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
Expand All @@ -32,9 +32,6 @@
@ExtendWith(MockitoExtension.class)
class CollectingBoxServiceTest {

private final double LATITUDE = 37.4888953606578;
private final double LONGITUDE = 126.901185398046;

private Point center;

@Value("${collecting-box.search.radius.meter}")
Expand All @@ -48,14 +45,14 @@ class CollectingBoxServiceTest {

@BeforeEach
void setUp() {
center = GeometryUtil.toPoint(LONGITUDE, LATITUDE);
center = GeometryUtil.toPoint(126.901185398046, 37.4888953606578);
}

@Test
@DisplayName("위도와 경도를 기준으로 특정 반경에 위치한 수거함 목록 조회 성공")
void findCollectingBoxesWithinArea_Success_withinArea() {
// given
List<Tag> tags = List.of(CLOTHES, LAMP_BATTERY, MEDICINE, TRASH);
Tags tags = new Tags(List.of(CLOTHES, LAMP_BATTERY, MEDICINE, TRASH));

CollectingBox box = CollectingBox.builder()
.id(1L)
Expand All @@ -64,11 +61,11 @@ void findCollectingBoxesWithinArea_Success_withinArea() {
.build();

// when
when(collectingBoxRepository.findAllWithinArea(center, radius, tags)).thenReturn(
when(collectingBoxRepository.findAllWithinArea(center, radius, tags.toUnmodifiableList())).thenReturn(
Collections.singletonList(box));

List<CollectingBoxResponse> result =
collectingBoxService.findCollectingBoxesWithinArea(LATITUDE, LONGITUDE, tags);
collectingBoxService.findCollectingBoxesWithinArea(center, tags);

// then
assertThat(result.get(0).getId()).isEqualTo(box.getId());
Expand All @@ -79,7 +76,7 @@ void findCollectingBoxesWithinArea_Success_withinArea() {
void findCollectingBoxesWithinArea_Fail_ByTagIsEmpty() {
// when, then
Assertions.assertThatThrownBy(
() -> collectingBoxService.findCollectingBoxesWithinArea(LATITUDE, LONGITUDE, List.of()))
() -> collectingBoxService.findCollectingBoxesWithinArea(center, new Tags(List.of())))
.isInstanceOf(CollectingBoxException.class)
.hasMessageContaining(ErrorCode.NOT_SELECTED_TAG.getMessage());
}
Expand Down
Loading