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

[FEAT] GPS-코드 변환 API, 동네 인증 API 개발 #78

Merged
merged 17 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1fb6b36
[FEAT] 동네 인증 관련 Custom Exception 클래스 구현
JIN-076 Feb 18, 2024
9775247
[FEAT] 동네 인증을 요청한 사용자와 토큰에 대한 유효성 검사 로직, 동네 인증을 요청한 법정구역코드에 대한 유효성 검사…
JIN-076 Feb 18, 2024
5c3fdaa
[FEAT] 서비스 지역 Geo 관련 Custom Exception 클래스 구현
JIN-076 Feb 18, 2024
6ff6630
[FEAT] 사용자의 GPS 좌표에 대한 법정구역코드를 요청하는 API 관련 Dto 클래스 구현
JIN-076 Feb 18, 2024
2361493
[FEAT] 사용자가 해당 서비스지역에 대한 동네 인증을 요청하는 API 관련 Dto 클래스 구현
JIN-076 Feb 18, 2024
544dc67
[FEAT] 사용자의 GPS 좌표에 대한 코드 요청, 서비스 지역에 대한 동네 인증 요청 API 구현
JIN-076 Feb 18, 2024
24dd573
[FEAT] 서비스 지역이 아닌 요청에 대한 에러코드 구현
JIN-076 Feb 18, 2024
190a73e
[FEAT] 비인증된 사용자의 동네 인증 요청에 대한 에러코드 구현
JIN-076 Feb 18, 2024
c71815f
[FEAT] 토큰과 사용자에 대한 유효성 검사 로직, 좌표에 대한 법정구역코드 반환 메서드 구현
JIN-076 Feb 18, 2024
d32067a
[REFACT] 서비스 불가 지역에 대한 에러코드 변경
JIN-076 Feb 18, 2024
5001c53
[REFACT] 사용자 GPS에 대한 법정구역코드 요청 시, 서비스 불가 지역에 대한 유효성 검사 추가
JIN-076 Feb 18, 2024
6680a6f
[FEAT] 동네 인증 API에 대한 Swagger 구현
JIN-076 Feb 18, 2024
e3e095c
[REFACT] GPS-코드 변환 API를 Geo Controller 패키지로 리팩토링, Geo 관련 API 구현, Geo …
JIN-076 Feb 18, 2024
26b03f5
[REFACT] 서비스 불가 지역에 대한 에러코드를 Cert에서 Geo로 이동
JIN-076 Feb 18, 2024
05e3774
[REFACT] 좌표-코드 변환 API를 GET 매핑, 쿼리 파라미터로 변경, 동네 인증 요청 API를 PATCH 매핑으로 …
JIN-076 Feb 18, 2024
145d446
[REFACT] 불필요한 Dto 삭제, 토큰과 사용자 불일치에 대한 에러코드 401 -> 403 변경
JIN-076 Feb 18, 2024
131b3aa
Merge branch 'develop' into feat/#13-authentication_neighborhood
JIN-076 Feb 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Member

Choose a reason for hiding this comment

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

LGTM 👍🏻

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.oeid.mogakgo.common.swagger.template;

import io.oeid.mogakgo.core.properties.swagger.error.SwaggerCertErrorExamples;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerGeoErrorExamples;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples;
import io.oeid.mogakgo.domain.cert.presentation.dto.req.UserRegionCertAPIReq;
import io.oeid.mogakgo.domain.cert.presentation.dto.res.UserRegionCertAPIRes;
import io.oeid.mogakgo.exception.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

@Tag(name = "Cert", description = "동네 인증 관련 API")
@SuppressWarnings("unused")
public interface CertSwagger {

@Operation(summary = "동네 인증 완료 응답", description = "동네 인증 완료를 요청할 때 사용하는 API"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "동네 인증 요청 성공",
content = @Content(schema = @Schema(implementation = UserRegionCertAPIRes.class))),
@ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E080101", value = SwaggerGeoErrorExamples.INVALID_SERVICE_REGION))),
@ApiResponse(responseCode = "401", description = "동네 인증 권한이 없음",
Copy link
Collaborator

Choose a reason for hiding this comment

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

보통 401은 아예 인증을 받지 않은 유저 (유저 정보 없음, 로그인 안함) 에 관한 상태코드인데 이 의미 맞나요?

content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E070201", value = SwaggerCertErrorExamples.INVALID_CERT_INFO))),
@ApiResponse(responseCode = "404", description = "요청한 유저가 존재하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND))),
})
ResponseEntity<UserRegionCertAPIRes> certificateNeighborhood(
@Parameter(hidden = true) Long userId,
UserRegionCertAPIReq request
);
}
Copy link
Member

Choose a reason for hiding this comment

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

LGTM 👍🏻

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.oeid.mogakgo.common.swagger.template;

import io.oeid.mogakgo.core.properties.swagger.error.SwaggerGeoErrorExamples;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples;
import io.oeid.mogakgo.domain.geo.presentation.dto.req.UserRegionInfoAPIReq;
import io.oeid.mogakgo.domain.geo.presentation.dto.res.UserRegionInfoAPIRes;
import io.oeid.mogakgo.exception.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

@Tag(name = "Geo", description = "지역 관련 API")
@SuppressWarnings("unused")
public interface GeoSwagger {

@Operation(summary = "GPS에 대한 법정구역코드 응답", description = "사용자의 GPS 좌표의 법정구역코드를 요청할 때 사용하는 API"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "법정구역코드 요청 성공",
content = @Content(schema = @Schema(implementation = UserRegionInfoAPIRes.class))),
@ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E080101", value = SwaggerGeoErrorExamples.INVALID_SERVICE_REGION))),
@ApiResponse(responseCode = "404", description = "요청한 유저가 존재하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND))),
})
ResponseEntity<UserRegionInfoAPIRes> getUserRegionInfoByGPS(
@Parameter(hidden = true) Long userId,
UserRegionInfoAPIReq request
);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.oeid.mogakgo.core.properties.swagger.error;

public class SwaggerCertErrorExamples {

public static final String INVALID_CERT_INFO = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":401,\"code\":\"E070201\",\"message\":\"동네 인증을 수행할 권한이 없습니다.\"}";
private SwaggerCertErrorExamples() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.oeid.mogakgo.core.properties.swagger.error;

public class SwaggerGeoErrorExamples {

public static final String INVALID_SERVICE_REGION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E080101\",\"message\":\"해당 지역은 서비스 지역이 아닙니다.\"}";
private SwaggerGeoErrorExamples() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.oeid.mogakgo.domain.cert.application;

import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_SERVICE_REGION;
import static io.oeid.mogakgo.exception.code.ErrorCode401.CERT_INVALID_INFORMATION;

import io.oeid.mogakgo.domain.cert.exception.CertException;
import io.oeid.mogakgo.domain.geo.domain.enums.Region;
import io.oeid.mogakgo.domain.geo.exception.GeoException;
import io.oeid.mogakgo.domain.user.application.UserCommonService;
import io.oeid.mogakgo.domain.user.application.UserGeoService;
import io.oeid.mogakgo.domain.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CertService {

private final UserGeoService userGeoService;
private final UserCommonService userCommonService;

public Long certificate(Long tokenUserId, Long userId, int areaCode) {
User tokenUser = validateToken(tokenUserId);
validateCertificator(tokenUser, userId);
Region region = validateAreaCodeCoverage(areaCode);
if (isPossibleCertification(userId, region)) {
userGeoService.updateUserGeo(userId, region);
}
return userId;
}

private User validateToken(Long userId) {
return userCommonService.getUserById(userId);
}

// 사용자가 아직 동네 인증을 하지 않았거나, 새롭게 인증하려는 지역이 이미 인증된 지역과 다를 경우만 동네 인증 처리
private boolean isPossibleCertification(Long userId, Region region) {
Region userRegionInfo = userGeoService.getUserGeo(userId).getRegion();
return userRegionInfo == null || userRegionInfo != region;
}

private void validateCertificator(User tokenUser, Long userId) {
if (!tokenUser.getId().equals(userId)) {
throw new CertException(CERT_INVALID_INFORMATION);
}
}

private Region validateAreaCodeCoverage(int areaCode) {
Region region = Region.getByAreaCode(areaCode);
if (region == null) {
throw new GeoException(INVALID_SERVICE_REGION);
}
return region;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.oeid.mogakgo.domain.cert.exception;

import io.oeid.mogakgo.exception.code.ErrorCode;
import io.oeid.mogakgo.exception.exception_class.CustomException;

public class CertException extends CustomException {

public CertException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.oeid.mogakgo.domain.cert.presentation;

import io.oeid.mogakgo.common.annotation.UserId;
import io.oeid.mogakgo.common.swagger.template.CertSwagger;
import io.oeid.mogakgo.domain.cert.application.CertService;
import io.oeid.mogakgo.domain.cert.presentation.dto.req.UserRegionCertAPIReq;
import io.oeid.mogakgo.domain.cert.presentation.dto.res.UserRegionCertAPIRes;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/cert")
@RequiredArgsConstructor
public class CertController implements CertSwagger {

private final CertService certService;

@PostMapping("/certificate")
Copy link
Member

Choose a reason for hiding this comment

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

Microsoft의 웹 API 디자인 모범 사례 - HTTP 메서드 측면에서 API 작업 정의에 따르면 지정된 URI에 리소스를 대체하는 용도로 PUT 메서드를 사용하는 방식을 제안하는데 이에 대한 생각은 어떠신가요?

public ResponseEntity<UserRegionCertAPIRes> certificateNeighborhood(
@UserId Long userId, @Valid @RequestBody UserRegionCertAPIReq request
) {
Long id = certService.certificate(userId, request.getUserId(), request.getAreaCode());
return ResponseEntity.ok(UserRegionCertAPIRes.from(id));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.oeid.mogakgo.domain.cert.presentation.dto.req;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;

@Schema(description = "사용자가 해당 코드에 해당하는 서비스 지역의 동네 인증을 요청")
@Getter
public class UserRegionCertAPIReq {

@Schema(description = "동네 인증을 요청한 사용자 ID", example = "2", implementation = Long.class)
@NotNull
private final Long userId;

@Schema(description = "동네 인증을 요청하는 서비스 지역의 법정구역코드", example = "11110", implementation = Integer.class)
@NotNull
private final Integer areaCode;

private UserRegionCertAPIReq(Long userId, Integer areaCode) {
this.userId = userId;
this.areaCode = areaCode;
}

public static UserRegionCertAPIReq of(Long userId, Integer areaCode) {
return new UserRegionCertAPIReq(userId, areaCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.oeid.mogakgo.domain.cert.presentation.dto.res;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;

@Schema(description = "동네 인증 완료 응답. 인증을 수행한 사용자의 ID를 반환한다.")
@Getter
public class UserRegionCertAPIRes {

@Schema(description = "동네 인증을 수행한 사용자 ID", example = "2", implementation = Long.class)
private final Long userId;

private UserRegionCertAPIRes(Long userId) {
this.userId = userId;
}

public static UserRegionCertAPIRes from(Long userId) {
return new UserRegionCertAPIRes(userId);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package io.oeid.mogakgo.domain.geo.application;

import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_SERVICE_REGION;

import io.oeid.mogakgo.core.properties.KakaoProperties;
import io.oeid.mogakgo.domain.cert.exception.CertException;
import io.oeid.mogakgo.domain.geo.domain.enums.Region;
import io.oeid.mogakgo.domain.geo.exception.GeoException;
import io.oeid.mogakgo.domain.geo.feign.KakaoFeignClient;
import io.oeid.mogakgo.domain.geo.feign.dto.AddressDocument;
import io.oeid.mogakgo.domain.geo.feign.dto.AddressInfoDto;
import io.oeid.mogakgo.domain.user.application.UserCommonService;
import io.oeid.mogakgo.domain.user.domain.User;
import io.oeid.mogakgo.exception.code.ErrorCode401;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -14,6 +22,25 @@ public class GeoService {
private static final String SEPERATOR = " ";
private final KakaoFeignClient kakaoFeignClient;
private final KakaoProperties kakaoProperties;
private final UserCommonService userCommonService;

public int getUserRegionInfoAboutCoordinates(Long tokenUserId, Long userId, Double x, Double y) {
User tokenUser = validateToken(tokenUserId);
validateUserExist(tokenUser, userId);
return validateCoordinatesCoverage(x, y);
}

private int validateCoordinatesCoverage(Double x, Double y) {
int areaCode = getAreaCodeAboutCoordinates(x, y);
validateCodeCoverage(areaCode);
return areaCode;
}

private void validateCodeCoverage(int areaCode) {
if (Region.getByAreaCode(areaCode) == null) {
throw new GeoException(INVALID_SERVICE_REGION);
}
}
Comment on lines +42 to +52
Copy link
Member

Choose a reason for hiding this comment

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

private 메서드들은 public 메서드 이하에 위치하면 좋을 것 같습니다! 👍🏻


public int getAreaCodeAboutCoordinates(Double x, Double y) {
AddressDocument document = getAddressInfoAboutAreaCode(x, y);
Expand All @@ -33,4 +60,15 @@ private String generateKey(KakaoProperties kakaoProperties) {
private int extractAreaCode(AddressDocument document) {
return Integer.parseInt(document.getCode().substring(0, 5));
}

private User validateToken(Long userId) {
return userCommonService.getUserById(userId);
}

private void validateUserExist(User tokenUser, Long userId) {
if (!tokenUser.getId().equals(userId)) {
throw new CertException(ErrorCode401.CERT_INVALID_INFORMATION);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.oeid.mogakgo.domain.geo.exception;

import io.oeid.mogakgo.exception.code.ErrorCode;
import io.oeid.mogakgo.exception.exception_class.CustomException;

public class GeoException extends CustomException {

public GeoException(ErrorCode errorCode) {
super(errorCode);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.oeid.mogakgo.domain.geo.presentation;

import io.oeid.mogakgo.common.annotation.UserId;
import io.oeid.mogakgo.common.swagger.template.GeoSwagger;
import io.oeid.mogakgo.domain.geo.presentation.dto.req.UserRegionInfoAPIReq;
import io.oeid.mogakgo.domain.geo.presentation.dto.res.UserRegionInfoAPIRes;
import io.oeid.mogakgo.domain.geo.application.GeoService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/geo")
@RequiredArgsConstructor
public class GeoController implements GeoSwagger {

private final GeoService geoService;

@PostMapping("/areacode")
Copy link
Member

Choose a reason for hiding this comment

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

위도 경도 데이터는 사용자 위치에 대한 정보기는 하지만 파라미터 공개가 사용자에게만 이루어지니까 Get에서 쿼리로 받는거도 괜찮을 것 같다는 생각이듭니다!

public ResponseEntity<UserRegionInfoAPIRes> getUserRegionInfoByGPS(
@UserId Long userId, @Valid @RequestBody UserRegionInfoAPIReq request
) {
int areaCode = geoService.getUserRegionInfoAboutCoordinates(
userId, request.getUserId(), request.getLongitude(), request.getLatitude()
);
return ResponseEntity.ok(UserRegionInfoAPIRes.from(areaCode));
}

}
Loading
Loading