-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 14 commits
1fb6b36
9775247
5c3fdaa
6ff6630
2361493
544dc67
24dd573
190a73e
c71815f
d32067a
5001c53
6680a6f
e3e095c
26b03f5
05e3774
145d446
131b3aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = "동네 인증 권한이 없음", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
); | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Microsoft의 웹 API 디자인 모범 사례 - HTTP 메서드 측면에서 API 작업 정의에 따르면 지정된 URI에 리소스를 대체하는 용도로 |
||
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; | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍🏻