From 2479c0bcf5b0be2420c940c6841373f6ee4247e6 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Wed, 4 Sep 2024 02:41:57 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8#31=20-=20swaager=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 컨트롤러 코드에서라도 최소한 Swagger 관련 어노테이션들을 걷어내기 인터페이스 사용 * JWT 토큰을 보내야하는 API에 공통으로 사용되는 스웨거 어노테이션들을 @AuthenticationCommonResponse로 응집 --- .../auth/controller/AuthController.java | 4 +- .../controller/request/AppleLoginRequest.java | 17 +- .../swagger/AuthControllerSwagger.java | 67 ++++++++ .../map/controller/MapController.java | 40 +++-- .../map/controller/request/RecordRequest.java | 15 +- .../request/UpdateRecordRequest.java | 14 +- .../swagger/AuthenticationCommonResponse.java | 50 ++++++ .../swagger/MapControllerSwagger.java | 157 ++++++++++++++++++ .../response/AttractionRecordResponse.java | 13 +- .../service/dto/response/RegionResponse.java | 19 ++- 10 files changed, 357 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/dnd/dndtravel/auth/controller/swagger/AuthControllerSwagger.java create mode 100644 src/main/java/com/dnd/dndtravel/map/controller/swagger/AuthenticationCommonResponse.java create mode 100644 src/main/java/com/dnd/dndtravel/map/controller/swagger/MapControllerSwagger.java diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java index 99dd550..c7584b6 100644 --- a/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java @@ -1,6 +1,7 @@ package com.dnd.dndtravel.auth.controller; import com.dnd.dndtravel.auth.controller.request.ReIssueTokenRequest; +import com.dnd.dndtravel.auth.controller.swagger.AuthControllerSwagger; import com.dnd.dndtravel.auth.service.dto.response.AppleIdTokenPayload; import com.dnd.dndtravel.auth.service.AppleOAuthService; import com.dnd.dndtravel.auth.service.JwtTokenService; @@ -19,12 +20,11 @@ @RequiredArgsConstructor @RestController -public class AuthController { +public class AuthController implements AuthControllerSwagger { private final AppleOAuthService appleOAuthService; private final JwtTokenService jwtTokenService; private final MemberService memberService; - //todo 클라이언트에서 실제 인증코드 보내주면 테스트 진행 필요 @PostMapping("/login/oauth2/apple") public ResponseEntity appleOAuthLogin(@RequestBody AppleLoginRequest appleLoginRequest) { // 클라이언트에서 준 code 값으로 apple의 IdToken Payload를 얻어온다 diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java b/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java index 4f76346..cc9bdd5 100644 --- a/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java +++ b/src/main/java/com/dnd/dndtravel/auth/controller/request/AppleLoginRequest.java @@ -1,9 +1,22 @@ package com.dnd.dndtravel.auth.controller.request; -//todo 필드값들 유효성 체크 -// todo 입력 색상 예시는 클라에서 String 타입들. RED, ORANGE, YELLOW, MELON, BLUE, PURPLE +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.dnd.dndtravel.auth.controller.request.validation.ColorValidation; +import com.dnd.dndtravel.member.domain.SelectedColor; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + public record AppleLoginRequest( + @Schema(description = "authorization code", requiredMode = REQUIRED) + @NotBlank(message = "authorization code는 필수 입니다.") + @Size(max = 300, message = "authorization code 형식이 아닙니다") String appleToken, + + @Schema(description = "유저가 선택한 색상", requiredMode = REQUIRED) + @ColorValidation(enumClass = SelectedColor.class) String selectedColor ){ } \ No newline at end of file diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/swagger/AuthControllerSwagger.java b/src/main/java/com/dnd/dndtravel/auth/controller/swagger/AuthControllerSwagger.java new file mode 100644 index 0000000..db30608 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/controller/swagger/AuthControllerSwagger.java @@ -0,0 +1,67 @@ +package com.dnd.dndtravel.auth.controller.swagger; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import com.dnd.dndtravel.auth.controller.request.AppleLoginRequest; +import com.dnd.dndtravel.auth.controller.request.ReIssueTokenRequest; +import com.dnd.dndtravel.auth.service.dto.response.ReissueTokenResponse; +import com.dnd.dndtravel.auth.service.dto.response.TokenResponse; + +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.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; + +@Tag(name = "auth", description = "인증 API") +public interface AuthControllerSwagger { + + String STATUS_CODE_400_BODY_MESSAGE = "{\"message\":\"잘못된 요청입니다\"}"; + + @Operation( + summary = "애플 OAuth login API" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 로그인", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)), + @ApiResponse(responseCode = "204", description = "refresh token 재발급 필요시", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)), + @ApiResponse( + responseCode = "500", description = "서버 오류", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = "{\"message\":\"Internal Server Error\"}") + ) + ) + }) + ResponseEntity appleOAuthLogin( + @Parameter(description = "로그인 요청 정보(authorization code, color)", required = true) + AppleLoginRequest appleLoginRequest + ); + + @Operation( + summary = "Refresh Token 재발급 API" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 발급", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ReissueTokenResponse.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 RefreshToken 요청시", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = STATUS_CODE_400_BODY_MESSAGE) + ) + ), + @ApiResponse( + responseCode = "500", description = "서버 오류", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = "{\"message\":\"Internal Server Error\"}") + ) + ) + }) + ReissueTokenResponse reissueToken( + @Parameter(description = "refreshToken", required = true) + ReIssueTokenRequest reissueTokenRequest + ); +} diff --git a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java index 34208e6..3478588 100644 --- a/src/main/java/com/dnd/dndtravel/map/controller/MapController.java +++ b/src/main/java/com/dnd/dndtravel/map/controller/MapController.java @@ -1,5 +1,6 @@ package com.dnd.dndtravel.map.controller; +import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -13,38 +14,35 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; +import com.dnd.dndtravel.config.AuthenticationMember; import com.dnd.dndtravel.map.controller.request.RecordRequest; import com.dnd.dndtravel.map.controller.request.UpdateRecordRequest; import com.dnd.dndtravel.map.controller.request.validation.PhotoValidation; -import com.dnd.dndtravel.config.AuthenticationMember; +import com.dnd.dndtravel.map.controller.swagger.MapControllerSwagger; import com.dnd.dndtravel.map.service.MapService; import com.dnd.dndtravel.map.service.dto.response.AttractionRecordDetailViewResponse; import com.dnd.dndtravel.map.service.dto.response.AttractionRecordResponse; import com.dnd.dndtravel.map.service.dto.response.RegionResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @Validated @RestController @RequiredArgsConstructor -public class MapController { +public class MapController implements MapControllerSwagger { private final MapService mapService; - @Tag(name = "MAP", description = "지도 API") - @Operation(summary = "전체 지역 조회", description = "전체 지역 방문 횟수를 조회합니다.") @GetMapping("/maps") public RegionResponse map(AuthenticationMember authenticationMember) { return mapService.allRegions(authenticationMember.id()); } - @PostMapping("/maps/record") + @PostMapping(value = "/maps/record", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public void memo( - @PhotoValidation @RequestPart("photos") List photos, - @RequestPart("recordRequest") RecordRequest recordRequest, - AuthenticationMember authenticationMember + AuthenticationMember authenticationMember, + @PhotoValidation @RequestPart(value = "photos", required = false) List photos, + @RequestPart("recordRequest") RecordRequest recordRequest ) { mapService.recordAttraction(recordRequest.toDto(photos), authenticationMember.id()); } @@ -53,9 +51,9 @@ public void memo( //서버에서 클라에 마지막 게시글의 ID를 줘야함 @GetMapping("/maps/history") public List findRecords( - @RequestParam long cursorNo, - @RequestParam(defaultValue = "10") int displayPerPage, - AuthenticationMember authenticationMember + AuthenticationMember authenticationMember, + @RequestParam(defaultValue = "0") long cursorNo, + @RequestParam(defaultValue = "10") int displayPerPage ) { return mapService.allRecords(authenticationMember.id(), cursorNo, displayPerPage); } @@ -63,28 +61,28 @@ public List findRecords( // 기록 단건 조회 @GetMapping("/maps/history/{recordId}") public AttractionRecordDetailViewResponse findRecord( + AuthenticationMember authenticationMember, @PathVariable long recordId ) { - long memberId = 1L; - return mapService.findOneVisitRecord(memberId, recordId); + return mapService.findOneVisitRecord(authenticationMember.id(), recordId); } - @PutMapping("/maps/history/{recordId}") + @PutMapping(value = "/maps/history/{recordId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public void updateRecord( + AuthenticationMember authenticationMember, @PathVariable long recordId, - @PhotoValidation @RequestPart("photos") List photos, + @PhotoValidation @RequestPart(value = "photos", required = false) List photos, @RequestPart("updateRecordRequest") UpdateRecordRequest updateRecordRequest ) { - long memberId = 1L; - mapService.updateVisitRecord(updateRecordRequest.toDto(photos), memberId, recordId); + mapService.updateVisitRecord(updateRecordRequest.toDto(photos), authenticationMember.id(), recordId); } // 기록 삭제 @DeleteMapping("/maps/history/{recordId}") public void deleteRecord( + AuthenticationMember authenticationMember, @PathVariable long recordId ) { - long memberId = 1L; - mapService.deleteRecord(memberId, recordId); + mapService.deleteRecord(authenticationMember.id(), recordId); } } diff --git a/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java b/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java index e0eb988..f8cd993 100644 --- a/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java +++ b/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java @@ -1,5 +1,8 @@ package com.dnd.dndtravel.map.controller.request; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + import java.time.LocalDate; import java.util.List; @@ -9,23 +12,25 @@ import com.dnd.dndtravel.map.controller.request.validation.RegionEnum; import com.dnd.dndtravel.map.dto.RecordDto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; -//todo 회의 후 제약조건 변경 필요 public record RecordRequest( + @Schema(description = "지역 이름", requiredMode = REQUIRED) @RegionEnum(enumClass = RegionCondition.class) String region, - @NotBlank(message = "명소 이름은 필수 입력 사항입니다.") - @Pattern(regexp = "^[가-힣]+$", message = "명소 이름은 한글만 입력 가능합니다.") - @Size(max = 50, message = "명소 이름은 50자 이내여야 합니다.") + @Schema(description = "명소명", requiredMode = REQUIRED) + @NotBlank(message = "명소명은 필수 입니다.") + @Size(max = 10, message = "명소 이름은 10자 이내여야 합니다.") String attractionName, + @Schema(description = "메모", requiredMode = NOT_REQUIRED) @Size(max = 25, message = "메모는 25자 이내여야 합니다.") String memo, + @Schema(description = "방문날짜, ISO Date(yyyy-MM-dd) 형식으로 입력", requiredMode = NOT_REQUIRED) @NotNull(message = "날짜는 필수 입력 사항입니다.") LocalDate localDate ) { diff --git a/src/main/java/com/dnd/dndtravel/map/controller/request/UpdateRecordRequest.java b/src/main/java/com/dnd/dndtravel/map/controller/request/UpdateRecordRequest.java index 4b4a3f9..6000940 100644 --- a/src/main/java/com/dnd/dndtravel/map/controller/request/UpdateRecordRequest.java +++ b/src/main/java/com/dnd/dndtravel/map/controller/request/UpdateRecordRequest.java @@ -1,5 +1,8 @@ package com.dnd.dndtravel.map.controller.request; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + import java.time.LocalDate; import java.util.List; @@ -9,23 +12,26 @@ import com.dnd.dndtravel.map.controller.request.validation.RegionEnum; import com.dnd.dndtravel.map.dto.RecordDto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public record UpdateRecordRequest( + @Schema(description = "지역 이름", requiredMode = REQUIRED) @RegionEnum(enumClass = RegionCondition.class) String region, - @NotBlank(message = "명소 이름은 필수 입력 사항입니다.") - @Pattern(regexp = "^[가-힣]+$", message = "명소 이름은 한글만 입력 가능합니다.") - @Size(max = 50, message = "명소 이름은 50자 이내여야 합니다.") + @Schema(description = "명소명", requiredMode = REQUIRED) + @NotBlank(message = "명소명은 필수 입니다.") + @Size(max = 10, message = "명소 이름은 10자 이내여야 합니다.") String attractionName, + @Schema(description = "메모", requiredMode = NOT_REQUIRED) @Size(max = 25, message = "메모는 25자 이내여야 합니다.") String memo, + @Schema(description = "방문날짜, ISO Date(yyyy-MM-dd) 형식으로 입력", requiredMode = NOT_REQUIRED) @NotNull(message = "날짜는 필수 입력 사항입니다.") LocalDate localDate ) { diff --git a/src/main/java/com/dnd/dndtravel/map/controller/swagger/AuthenticationCommonResponse.java b/src/main/java/com/dnd/dndtravel/map/controller/swagger/AuthenticationCommonResponse.java new file mode 100644 index 0000000..d6c2c60 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/controller/swagger/AuthenticationCommonResponse.java @@ -0,0 +1,50 @@ +package com.dnd.dndtravel.map.controller.swagger; + +import static io.swagger.v3.oas.annotations.enums.ParameterIn.HEADER; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.http.MediaType; + +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.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Operation( + parameters = { + @Parameter( + description = "Bearer JWT 토큰", + required = true, + in = HEADER + ) + } +) +@ApiResponses(value = { + @ApiResponse(responseCode = "400", description = "토큰 헤더가 비어있거나, Bearer Token 형식이 아닌경우", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = "{\"message\":\"잘못된 요청입니다\"}") + ) + ), + @ApiResponse( + responseCode = "401", description = "토큰의 유저정보가 유효하지 않거나 토큰 해독 실패", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = "{\"message\": \"유효하지 않은 토큰값입니다.\"}") + ) + ), + @ApiResponse( + responseCode = "500", description = "서버 오류", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = "{\"message\":\"Internal Server Error\"}") + ) + ) +}) +public @interface AuthenticationCommonResponse { +} diff --git a/src/main/java/com/dnd/dndtravel/map/controller/swagger/MapControllerSwagger.java b/src/main/java/com/dnd/dndtravel/map/controller/swagger/MapControllerSwagger.java new file mode 100644 index 0000000..9c2786a --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/controller/swagger/MapControllerSwagger.java @@ -0,0 +1,157 @@ +package com.dnd.dndtravel.map.controller.swagger; + +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; + +import com.dnd.dndtravel.config.AuthenticationMember; +import com.dnd.dndtravel.map.controller.request.RecordRequest; +import com.dnd.dndtravel.map.controller.request.UpdateRecordRequest; +import com.dnd.dndtravel.map.service.dto.response.AttractionRecordDetailViewResponse; +import com.dnd.dndtravel.map.service.dto.response.AttractionRecordResponse; +import com.dnd.dndtravel.map.service.dto.response.RegionResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +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; + +@Tag(name = "map", description = "지도 API") +public interface MapControllerSwagger { + + String STATUS_CODE_400_BODY_MESSAGE = "{\"message\":\"잘못된 요청입니다\"}"; + + @Operation( + summary = "인증된 사용자의 모든 지역 정보 조회" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 조회", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = RegionResponse.class) + ) + ), + @ApiResponse(responseCode = "400", description = "유저가 존재하지 않는경우", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = STATUS_CODE_400_BODY_MESSAGE) + ) + ), + }) + @AuthenticationCommonResponse + RegionResponse map( + @Parameter(hidden = true) + AuthenticationMember authenticationMember + ); + + + @Operation( + summary = "인증된 사용자의 방문 기록 저장" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 저장"), + @ApiResponse(responseCode = "400", description = "잘못된 방문기록 값 입력, 지역이나 유저정보가 유효하지 않는경우", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = STATUS_CODE_400_BODY_MESSAGE) + ) + ), + }) + @AuthenticationCommonResponse + void memo( + @Parameter(hidden = true) + AuthenticationMember authenticationMember, + @Parameter(description = "사진") + List photos, + @Parameter(description = "기록 요청 정보", required = true) + RecordRequest recordRequest + ); + + @Operation( + summary = "인증된 사용자의 방문 기록 조회" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 조회", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + array = @ArraySchema(schema = @Schema(implementation = AttractionRecordResponse.class)))), + @ApiResponse(responseCode = "400", description = "잘못된 방문기록 값 입력, 유저정보가 유효하지 않은경우", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = STATUS_CODE_400_BODY_MESSAGE) + ) + ), + }) + @AuthenticationCommonResponse + List findRecords( + @Parameter(hidden = true) + AuthenticationMember authenticationMember, + @Parameter(description = "게시글의 ID값, 0 혹은 미입력시 가장최신 페이지 조회", example = "이전요청의 마지막 게시글ID가 7인경우 7로 요청시 다음 페이지 게시글 조회") + long cursorNo, + @Parameter(description = "페이지당 조회할 게시글 개수, 미입력시 10으로 지정") + int displayPerPage + ); + + @Operation( + summary = "인증된 사용자의 방문 기록 단건 조회" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상조회, 단일 JSON객체 반환", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = AttractionRecordDetailViewResponse.class))), + @ApiResponse(responseCode = "400", description = "유저정보나 방문기록이 유효하지 않은경우", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = STATUS_CODE_400_BODY_MESSAGE) + ) + ), + }) + @AuthenticationCommonResponse + AttractionRecordDetailViewResponse findRecord( + @Parameter(hidden = true) + AuthenticationMember authenticationMember, + @Parameter(description = "방문기록 id값", required = true) + long recordId + ); + + @Operation( + summary = "인증된 사용자의 방문 기록 수정" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 수정"), + @ApiResponse(responseCode = "400", description = "유저정보나 방문기록, 명소정보가 유효하지 않은경우", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = STATUS_CODE_400_BODY_MESSAGE) + ) + ), + }) + @AuthenticationCommonResponse + void updateRecord( + @Parameter(hidden = true) + AuthenticationMember authenticationMember, + @Parameter(description = "방문기록 id값", required = true) + long recordId, + @Parameter(description = "수정요청한 사진", required = false) + List photos, + @Parameter(description = "수정요청 값", required = true) + UpdateRecordRequest updateRecordRequest + ); + + @Operation( + summary = "인증된 사용자의 방문 기록 삭제" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 삭제"), + @ApiResponse(responseCode = "400", description = "삭제하려는 방문기록이 유효하지 않은경우", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(example = STATUS_CODE_400_BODY_MESSAGE) + ) + ), + }) + @AuthenticationCommonResponse + void deleteRecord( + @Parameter(hidden = true) + AuthenticationMember authenticationMember, + @Parameter(description = "방문기록 id값", required = true) + long recordId + ); +} diff --git a/src/main/java/com/dnd/dndtravel/map/service/dto/response/AttractionRecordResponse.java b/src/main/java/com/dnd/dndtravel/map/service/dto/response/AttractionRecordResponse.java index 4e8211d..45db14e 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/dto/response/AttractionRecordResponse.java +++ b/src/main/java/com/dnd/dndtravel/map/service/dto/response/AttractionRecordResponse.java @@ -1,20 +1,31 @@ package com.dnd.dndtravel.map.service.dto.response; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + import java.time.LocalDate; import java.util.List; import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; @Builder public record AttractionRecordResponse( + @Schema(description = "방문기록 id", requiredMode = REQUIRED) long id, + @Schema(description = "방문기록 전체개수", requiredMode = REQUIRED) long entireRecordCount, + @Schema(description = "지역명", requiredMode = REQUIRED) + String region, + @Schema(description = "명소명", requiredMode = REQUIRED) String attractionName, + @Schema(description = "메모", requiredMode = NOT_REQUIRED) String memo, + @Schema(description = "방문날짜, ISO Date(yyyy-MM-dd)", requiredMode = NOT_REQUIRED) LocalDate visitDate, - String region, + @Schema(description = "이미지 URL들", requiredMode = NOT_REQUIRED) List photoUrls ) { public static AttractionRecordResponse from(RecordProjection recordProjection) { diff --git a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java index 00fce0b..9f36e8a 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java +++ b/src/main/java/com/dnd/dndtravel/map/service/dto/response/RegionResponse.java @@ -1,15 +1,26 @@ package com.dnd.dndtravel.map.service.dto.response; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + import com.dnd.dndtravel.map.service.dto.RegionDto; import com.dnd.dndtravel.member.domain.SelectedColor; import java.util.List; +import io.swagger.v3.oas.annotations.media.Schema; + public record RegionResponse( - List regions, // 지역별 opacity 정보, 땅 이름 - int visitCount, // 방문 지도 개수 - int totalCount, // 전체 땅 개수 - SelectedColor selectedColor // 선택된 컬러 + @Schema(description = "지역의 이름, opacity", requiredMode = REQUIRED) + List regions, + + @Schema(description = "방문 지역 개수", requiredMode = REQUIRED) + int visitCount, + + @Schema(description = "전체 지역 개수", requiredMode = REQUIRED) + int totalCount, + + @Schema(description = "유저가 선택한 색상", requiredMode = REQUIRED) + SelectedColor selectedColor ) { private static final int TOTAL_COUNT = 16; // 전체 지역구의 개수, 변경가능성이 낮아 16이라는 상수로 고정 From c2ba6455788347f63a4232dcadd059c411ce8ff4 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Wed, 4 Sep 2024 02:43:26 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=A8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20R?= =?UTF-8?q?equest=20=ED=95=84=EB=93=9C=EA=B0=92=EB=93=A4=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 * 클라이언트가 보내주는 유저 색상과 authorization code에 대한 검증 --- .../request/validation/ColorValidation.java | 16 ++++++++ .../request/validation/ColorValidator.java | 37 +++++++++++++++++++ .../member/domain/SelectedColor.java | 5 +++ 3 files changed, 58 insertions(+) create mode 100644 src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidation.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidator.java diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidation.java b/src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidation.java new file mode 100644 index 0000000..b8c5961 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidation.java @@ -0,0 +1,16 @@ +package com.dnd.dndtravel.auth.controller.request.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; + +@Constraint(validatedBy = {ColorValidator.class}) +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ColorValidation { + String message() default "invalid region"; + Class> enumClass(); +} diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidator.java b/src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidator.java new file mode 100644 index 0000000..b9b7571 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/controller/request/validation/ColorValidator.java @@ -0,0 +1,37 @@ +package com.dnd.dndtravel.auth.controller.request.validation; + +import java.util.Arrays; + +import com.dnd.dndtravel.map.controller.request.validation.RegionCondition; +import com.dnd.dndtravel.map.controller.request.validation.RegionEnum; +import com.dnd.dndtravel.member.domain.SelectedColor; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class ColorValidator implements ConstraintValidator { + + private ColorValidation annotation; + + + @Override + public void initialize(ColorValidation constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + @Override + public boolean isValid(String color, ConstraintValidatorContext context) { + // 색상 없으면 예외 + if (color == null || color.isBlank()) { + return false; + } + + Object[] enumValues = this.annotation.enumClass().getEnumConstants(); + if (enumValues == null) { + return false; + } + + // 색상 Enum중 하나라도 해당되면 true + return Arrays.stream(enumValues).anyMatch(enumValue -> SelectedColor.isMatch(color)); + } +} diff --git a/src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java b/src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java index 61dc5a1..cd6fd13 100644 --- a/src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java +++ b/src/main/java/com/dnd/dndtravel/member/domain/SelectedColor.java @@ -24,4 +24,9 @@ public static SelectedColor convertToEnum(String selectedColor) { .filter(color -> color.getValue().equalsIgnoreCase(selectedColor)) .findAny().orElseThrow(() -> new RuntimeException("존재하지 않는 색상입니다")); } + + public static boolean isMatch(String color) { + return Arrays.stream(SelectedColor.values()) + .anyMatch(selectedColor -> selectedColor.getValue().equals(color)); + } } From 33bb4506518b29dfd0e48ed98e327ab6538faf93 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Wed, 4 Sep 2024 02:44:04 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=94=A5=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import,=20=EC=93=B0=EC=9D=B4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AppleOAuthService.java | 1 - .../auth/service/JwtTokenService.java | 1 - .../dnd/dndtravel/config/SwaggerConfig.java | 2 +- .../com/dnd/dndtravel/map/domain/Region.java | 6 ------ .../dndtravel/map/domain/VisitOpacity.java | 19 ------------------- .../map/repository/PhotoRepository.java | 2 -- .../MemberAttractionRepositoryCustom.java | 3 --- .../dndtravel/map/service/PhotoService.java | 1 - 8 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 src/main/java/com/dnd/dndtravel/map/domain/VisitOpacity.java diff --git a/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java b/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java index 8145dd5..078d687 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/AppleOAuthService.java @@ -43,7 +43,6 @@ @RequiredArgsConstructor @Component -@Slf4j public class AppleOAuthService { private final AppleClient appleClient; private final AppleProperties appleProperties; diff --git a/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java index 79b70cb..b34f286 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java @@ -16,7 +16,6 @@ public class JwtTokenService { private final JwtProvider jwtProvider; private final RefreshTokenRepository refreshTokenRepository; - // todo 현재 refreshtoken하나로 계속해서 발급해주는 모델인데, 이는 리프레쉬 탈취시 보안위협있음. 추후 개선 필요 @Transactional public TokenResponse generateTokens(Long memberId) { RefreshToken refreshToken = refreshTokenRepository.findByMemberId(memberId); diff --git a/src/main/java/com/dnd/dndtravel/config/SwaggerConfig.java b/src/main/java/com/dnd/dndtravel/config/SwaggerConfig.java index d7ff76f..2429906 100644 --- a/src/main/java/com/dnd/dndtravel/config/SwaggerConfig.java +++ b/src/main/java/com/dnd/dndtravel/config/SwaggerConfig.java @@ -7,6 +7,7 @@ @Configuration public class SwaggerConfig { + @Bean public OpenAPI openAPI() { return new OpenAPI() @@ -14,6 +15,5 @@ public OpenAPI openAPI() { .title("MAPDDANG API") .description("맵땅 앱 관련 API") .version("1.0.0")); - } } diff --git a/src/main/java/com/dnd/dndtravel/map/domain/Region.java b/src/main/java/com/dnd/dndtravel/map/domain/Region.java index 5af1273..d7ce37d 100644 --- a/src/main/java/com/dnd/dndtravel/map/domain/Region.java +++ b/src/main/java/com/dnd/dndtravel/map/domain/Region.java @@ -1,8 +1,6 @@ package com.dnd.dndtravel.map.domain; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -28,8 +26,4 @@ public static Region of(String name) { private Region(String name) { this.name = name; } - - public boolean isEqualTo(String name) { - return this.name.equals(name); - } } diff --git a/src/main/java/com/dnd/dndtravel/map/domain/VisitOpacity.java b/src/main/java/com/dnd/dndtravel/map/domain/VisitOpacity.java deleted file mode 100644 index c88bbd7..0000000 --- a/src/main/java/com/dnd/dndtravel/map/domain/VisitOpacity.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.dnd.dndtravel.map.domain; - -public enum VisitOpacity { - ZERO(0), ONE(1), TWO(2), THREE(3); - - private final int value; - - VisitOpacity(int value) { - this.value = value; - } - - public boolean isNotZero() { - return this != ZERO; - } - - public int toInt() { - return this.value; - } -} diff --git a/src/main/java/com/dnd/dndtravel/map/repository/PhotoRepository.java b/src/main/java/com/dnd/dndtravel/map/repository/PhotoRepository.java index d6485be..8963892 100644 --- a/src/main/java/com/dnd/dndtravel/map/repository/PhotoRepository.java +++ b/src/main/java/com/dnd/dndtravel/map/repository/PhotoRepository.java @@ -1,8 +1,6 @@ package com.dnd.dndtravel.map.repository; import java.util.List; -import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; import com.dnd.dndtravel.map.domain.Photo; diff --git a/src/main/java/com/dnd/dndtravel/map/repository/custom/MemberAttractionRepositoryCustom.java b/src/main/java/com/dnd/dndtravel/map/repository/custom/MemberAttractionRepositoryCustom.java index c630373..b855911 100644 --- a/src/main/java/com/dnd/dndtravel/map/repository/custom/MemberAttractionRepositoryCustom.java +++ b/src/main/java/com/dnd/dndtravel/map/repository/custom/MemberAttractionRepositoryCustom.java @@ -1,9 +1,6 @@ package com.dnd.dndtravel.map.repository.custom; import java.util.List; -import java.util.Optional; - -import com.dnd.dndtravel.map.domain.MemberAttraction; import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection; public interface MemberAttractionRepositoryCustom { diff --git a/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java b/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java index 4424ca3..6a7fc1e 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java @@ -16,7 +16,6 @@ import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; -import com.dnd.dndtravel.map.domain.Photo; import lombok.RequiredArgsConstructor; From e55f3e9d1f0222b4927862550c4be10cb6bc02d8 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:59:36 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8#34=20-=20ExceptionHandler=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0=20Custom=20Exception?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppleTokenDecodingException.java | 9 ++ .../exception/JwtTokenDecodingException.java | 11 ++ .../exception/JwtTokenExpiredException.java | 11 ++ .../RefreshTokenInvalidException.java | 9 ++ .../dndtravel/auth/service/JwtProvider.java | 7 +- .../auth/service/JwtTokenService.java | 3 +- .../dndtravel/auth/service/TokenDecoder.java | 3 +- .../common/CommonExceptionHandler.java | 125 ++++++++++++++++++ .../MemberAttractionNotFoundException.java | 9 ++ .../exception/MemberNotFoundException.java | 9 ++ .../exception/PhotoDeleteFailException.java | 11 ++ .../map/exception/PhotoEmptyException.java | 9 ++ .../map/exception/PhotoInvalidException.java | 8 ++ .../exception/PhotoUploadFailException.java | 19 +++ .../exception/RegionNotFoundException.java | 9 ++ .../dnd/dndtravel/map/service/MapService.java | 21 +-- .../dndtravel/map/service/PhotoService.java | 17 ++- 17 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/dnd/dndtravel/auth/exception/AppleTokenDecodingException.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenDecodingException.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenExpiredException.java create mode 100644 src/main/java/com/dnd/dndtravel/auth/exception/RefreshTokenInvalidException.java create mode 100644 src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java create mode 100644 src/main/java/com/dnd/dndtravel/map/exception/MemberAttractionNotFoundException.java create mode 100644 src/main/java/com/dnd/dndtravel/map/exception/MemberNotFoundException.java create mode 100644 src/main/java/com/dnd/dndtravel/map/exception/PhotoDeleteFailException.java create mode 100644 src/main/java/com/dnd/dndtravel/map/exception/PhotoEmptyException.java create mode 100644 src/main/java/com/dnd/dndtravel/map/exception/PhotoInvalidException.java create mode 100644 src/main/java/com/dnd/dndtravel/map/exception/PhotoUploadFailException.java create mode 100644 src/main/java/com/dnd/dndtravel/map/exception/RegionNotFoundException.java diff --git a/src/main/java/com/dnd/dndtravel/auth/exception/AppleTokenDecodingException.java b/src/main/java/com/dnd/dndtravel/auth/exception/AppleTokenDecodingException.java new file mode 100644 index 0000000..935bc0c --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/exception/AppleTokenDecodingException.java @@ -0,0 +1,9 @@ +package com.dnd.dndtravel.auth.exception; + +public class AppleTokenDecodingException extends RuntimeException { + private static final String MESSAGE = "Apple 토큰 payload 해독실패"; + + public AppleTokenDecodingException(Exception e) { + super(MESSAGE, e); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenDecodingException.java b/src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenDecodingException.java new file mode 100644 index 0000000..b3ecc54 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenDecodingException.java @@ -0,0 +1,11 @@ +package com.dnd.dndtravel.auth.exception; + +import io.jsonwebtoken.JwtException; + +public class JwtTokenDecodingException extends RuntimeException { + private static final String MESSAGE = "Jwt 토큰 해독 실패"; + + public JwtTokenDecodingException(JwtException e) { + super(MESSAGE, e); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenExpiredException.java b/src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenExpiredException.java new file mode 100644 index 0000000..6f3af49 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/exception/JwtTokenExpiredException.java @@ -0,0 +1,11 @@ +package com.dnd.dndtravel.auth.exception; + +import io.jsonwebtoken.ExpiredJwtException; + +public class JwtTokenExpiredException extends RuntimeException { + private static final String MESSAGE = "Jwt 토큰이 만료되었음"; + + public JwtTokenExpiredException(ExpiredJwtException e) { + super(MESSAGE, e); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/exception/RefreshTokenInvalidException.java b/src/main/java/com/dnd/dndtravel/auth/exception/RefreshTokenInvalidException.java new file mode 100644 index 0000000..6f18232 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/exception/RefreshTokenInvalidException.java @@ -0,0 +1,9 @@ +package com.dnd.dndtravel.auth.exception; + +public class RefreshTokenInvalidException extends RuntimeException { + private static final String MESSAGE = "유효하지 않은 RefreshToken 토큰 [refreshToken=%s]"; + + public RefreshTokenInvalidException(String refreshToken) { + super(String.format(MESSAGE, refreshToken)); + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java b/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java index abe324d..3b43922 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java @@ -11,6 +11,9 @@ import javax.crypto.SecretKey; +import com.dnd.dndtravel.auth.exception.JwtTokenExpiredException; +import com.dnd.dndtravel.auth.exception.JwtTokenDecodingException; + @Component public class JwtProvider { private static final String CLAIM_CONTENT = "memberId"; @@ -54,9 +57,9 @@ public Claims parseClaims(String splitHeader) { .parseSignedClaims(splitHeader); } catch (ExpiredJwtException e) { - throw new RuntimeException("message", e); // 유효하지 않은 토큰 + throw new JwtTokenExpiredException(e); } catch (JwtException e) { - throw new RuntimeException("message", e); // 토큰 해독 실패 + throw new JwtTokenDecodingException(e); } return claims.getPayload(); diff --git a/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java index 79b70cb..e35565d 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java @@ -4,6 +4,7 @@ import org.springframework.transaction.annotation.Transactional; import com.dnd.dndtravel.auth.domain.RefreshToken; +import com.dnd.dndtravel.auth.exception.RefreshTokenInvalidException; import com.dnd.dndtravel.auth.repository.RefreshTokenRepository; import com.dnd.dndtravel.auth.service.dto.response.TokenResponse; import com.dnd.dndtravel.auth.service.dto.response.ReissueTokenResponse; @@ -35,7 +36,7 @@ public TokenResponse generateTokens(Long memberId) { @Transactional public ReissueTokenResponse reIssue(String token) { //validation - RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token).orElseThrow(() -> new RuntimeException("유효하지 않은 토큰")); + RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token).orElseThrow(() -> new RefreshTokenInvalidException(token)); //RTR refreshTokenRepository.delete(refreshToken); diff --git a/src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java b/src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java index a789b2f..21c3f9f 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/TokenDecoder.java @@ -2,6 +2,7 @@ import java.util.Base64; +import com.dnd.dndtravel.auth.exception.AppleTokenDecodingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,7 +26,7 @@ public static T decodePayload(String token, Class targetClass) { try { return objectMapper.readValue(payload, targetClass); } catch (Exception e) { - throw new RuntimeException("Error decoding token payload", e); + throw new AppleTokenDecodingException(e); } } } diff --git a/src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java b/src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java new file mode 100644 index 0000000..213f090 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java @@ -0,0 +1,125 @@ +package com.dnd.dndtravel.common; + +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 com.dnd.dndtravel.auth.exception.AppleTokenDecodingException; +import com.dnd.dndtravel.auth.exception.JwtTokenDecodingException; +import com.dnd.dndtravel.auth.exception.JwtTokenExpiredException; +import com.dnd.dndtravel.auth.exception.RefreshTokenInvalidException; +import com.dnd.dndtravel.map.exception.MemberAttractionNotFoundException; +import com.dnd.dndtravel.map.exception.MemberNotFoundException; +import com.dnd.dndtravel.map.exception.PhotoDeleteFailException; +import com.dnd.dndtravel.map.exception.PhotoEmptyException; +import com.dnd.dndtravel.map.exception.PhotoInvalidException; +import com.dnd.dndtravel.map.exception.PhotoUploadFailException; +import com.dnd.dndtravel.map.exception.RegionNotFoundException; +//todo 예외클래스가 많아지면 해당클래스가 길어질것으로 예상, 개선필요해보이고 보안 때문에 상태코드별로 애매하게 동일한 메시지를 전달해주고, 스웨거 문서로 상세 오류를 전달해주는데 이 구조가 적절한건지 고민해봐야한다. +@RestControllerAdvice +public class CommonExceptionHandler { + + private static final String INTERNAL_SERVER_ERROR_MESSAGE = "Internal Server Error"; + private static final String BAD_REQUEST_MESSAGE = "잘못된 요청입니다"; + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity runtimeException() { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(BAD_REQUEST_MESSAGE); + } + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity runtimeException(MemberNotFoundException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(BAD_REQUEST_MESSAGE); + } + + @ExceptionHandler(MemberAttractionNotFoundException.class) + public ResponseEntity runtimeException(MemberAttractionNotFoundException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(BAD_REQUEST_MESSAGE); + } + + @ExceptionHandler(RegionNotFoundException.class) + public ResponseEntity runtimeException(RegionNotFoundException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(BAD_REQUEST_MESSAGE); + } + + @ExceptionHandler(AppleTokenDecodingException.class) + public ResponseEntity runtimeException(AppleTokenDecodingException e) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("토큰 인증에 실패했습니다"); + } + + @ExceptionHandler(JwtTokenExpiredException.class) + public ResponseEntity runtimeException(JwtTokenExpiredException e) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("토큰 인증에 실패했습니다"); + } + + @ExceptionHandler(JwtTokenDecodingException.class) + public ResponseEntity runtimeException(JwtTokenDecodingException e) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("토큰 인증에 실패했습니다"); + } + + @ExceptionHandler(RefreshTokenInvalidException.class) + public ResponseEntity runtimeException(RefreshTokenInvalidException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(BAD_REQUEST_MESSAGE); + } + + @ExceptionHandler(PhotoEmptyException.class) + public ResponseEntity runtimeException(PhotoEmptyException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(BAD_REQUEST_MESSAGE); + } + + @ExceptionHandler(PhotoDeleteFailException.class) + public ResponseEntity runtimeException(PhotoDeleteFailException e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(INTERNAL_SERVER_ERROR_MESSAGE); + } + + @ExceptionHandler(PhotoUploadFailException.class) + public ResponseEntity runtimeException(PhotoUploadFailException e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(INTERNAL_SERVER_ERROR_MESSAGE); + } + + @ExceptionHandler(PhotoInvalidException.class) + public ResponseEntity runtimeException(PhotoInvalidException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(BAD_REQUEST_MESSAGE); + } + + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity runtimeException(RuntimeException e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(INTERNAL_SERVER_ERROR_MESSAGE); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity runtimeException(Exception e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(INTERNAL_SERVER_ERROR_MESSAGE); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/exception/MemberAttractionNotFoundException.java b/src/main/java/com/dnd/dndtravel/map/exception/MemberAttractionNotFoundException.java new file mode 100644 index 0000000..8082de3 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/exception/MemberAttractionNotFoundException.java @@ -0,0 +1,9 @@ +package com.dnd.dndtravel.map.exception; + +public class MemberAttractionNotFoundException extends RuntimeException { + private static final String MESSAGE = "존재하지 않는 방문기록 [memberAttractionId=%s]"; + + public MemberAttractionNotFoundException(long memberAttractionId) { + super(String.format(MESSAGE, memberAttractionId)); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/exception/MemberNotFoundException.java b/src/main/java/com/dnd/dndtravel/map/exception/MemberNotFoundException.java new file mode 100644 index 0000000..fd78ecb --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/exception/MemberNotFoundException.java @@ -0,0 +1,9 @@ +package com.dnd.dndtravel.map.exception; + +public class MemberNotFoundException extends RuntimeException { + private static final String MESSAGE = "존재하지 않는 유저 [memberId=%s]"; + + public MemberNotFoundException(long memberId) { + super(String.format(MESSAGE, memberId)); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/exception/PhotoDeleteFailException.java b/src/main/java/com/dnd/dndtravel/map/exception/PhotoDeleteFailException.java new file mode 100644 index 0000000..5b9c3c2 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/exception/PhotoDeleteFailException.java @@ -0,0 +1,11 @@ +package com.dnd.dndtravel.map.exception; + +import com.amazonaws.SdkClientException; + +public class PhotoDeleteFailException extends RuntimeException{ + private static final String MESSAGE = "s3 이미지 삭제 실패 [imagePath=%s]"; + + public PhotoDeleteFailException(String existingFileName, SdkClientException e) { + super(String.format(MESSAGE,existingFileName), e); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/exception/PhotoEmptyException.java b/src/main/java/com/dnd/dndtravel/map/exception/PhotoEmptyException.java new file mode 100644 index 0000000..307af0e --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/exception/PhotoEmptyException.java @@ -0,0 +1,9 @@ +package com.dnd.dndtravel.map.exception; + +public class PhotoEmptyException extends RuntimeException{ + private static final String MESSAGE = "이미지가 존재하지 않음"; + + public PhotoEmptyException() { + super(MESSAGE); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/exception/PhotoInvalidException.java b/src/main/java/com/dnd/dndtravel/map/exception/PhotoInvalidException.java new file mode 100644 index 0000000..f558cbd --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/exception/PhotoInvalidException.java @@ -0,0 +1,8 @@ +package com.dnd.dndtravel.map.exception; + +public class PhotoInvalidException extends RuntimeException { + + public PhotoInvalidException(String message) { + super(message); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/exception/PhotoUploadFailException.java b/src/main/java/com/dnd/dndtravel/map/exception/PhotoUploadFailException.java new file mode 100644 index 0000000..0c4fc2a --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/exception/PhotoUploadFailException.java @@ -0,0 +1,19 @@ +package com.dnd.dndtravel.map.exception; + +import java.io.IOException; + +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.SdkClientException; + +public class PhotoUploadFailException extends RuntimeException { + private static final String MESSAGE = "s3 이미지 업로드 실패 [image=%s]"; + + public PhotoUploadFailException(MultipartFile image, IOException e) { + super(String.format(MESSAGE, image), e); + } + + public PhotoUploadFailException(String image, SdkClientException e) { + super(String.format(MESSAGE, image), e); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/exception/RegionNotFoundException.java b/src/main/java/com/dnd/dndtravel/map/exception/RegionNotFoundException.java new file mode 100644 index 0000000..7dae336 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/map/exception/RegionNotFoundException.java @@ -0,0 +1,9 @@ +package com.dnd.dndtravel.map.exception; + +public class RegionNotFoundException extends RuntimeException { + private static final String MESSAGE = "존재하지 않는 지역 [region=%s]"; + + public RegionNotFoundException(String region) { + super(String.format(MESSAGE, region)); + } +} diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 17c6375..7caabce 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -15,6 +15,9 @@ import com.dnd.dndtravel.map.domain.MemberAttraction; import com.dnd.dndtravel.map.domain.MemberRegion; import com.dnd.dndtravel.map.domain.Region; +import com.dnd.dndtravel.map.exception.MemberAttractionNotFoundException; +import com.dnd.dndtravel.map.exception.MemberNotFoundException; +import com.dnd.dndtravel.map.exception.RegionNotFoundException; import com.dnd.dndtravel.map.repository.dto.projection.AttractionPhotoProjection; import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection; import com.dnd.dndtravel.map.service.dto.RegionDto; @@ -45,8 +48,7 @@ public class MapService { @Transactional(readOnly = true) public RegionResponse allRegions(long memberId) { - //todo custom ex - Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저")); + Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId)); // 유저의 지역 방문기록 전부 가져온다 List memberRegions = memberRegionRepository.findByMemberId(memberId); @@ -70,8 +72,8 @@ public RegionResponse allRegions(long memberId) { @Transactional public void recordAttraction(RecordDto recordDto, long memberId) { // validate - Region region = regionRepository.findByName(recordDto.region()).orElseThrow(() -> new RuntimeException("존재하지 않는 지역")); - Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저")); + Region region = regionRepository.findByName(recordDto.region()).orElseThrow(() -> new RegionNotFoundException(recordDto.region())); + Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId)); Attraction attraction = attractionRepository.save(Attraction.of(region, recordDto.attractionName())); @@ -90,7 +92,7 @@ public void recordAttraction(RecordDto recordDto, long memberId) { @Transactional(readOnly = true) public List allRecords(long memberId, long cursorNo, int displayPerPage) { //validation - Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저")); + Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId)); List memberAttractions = memberAttractionRepository.findByMemberId(memberId); if (memberAttractions.isEmpty()) { return List.of(); @@ -115,8 +117,9 @@ public List allRecords(long memberId, long cursorNo, i //기록 단건 조회 @Transactional(readOnly = true) public AttractionRecordDetailViewResponse findOneVisitRecord(long memberId, long memberAttractionId) { - Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저")); - MemberAttraction memberAttraction = memberAttractionRepository.findById(memberAttractionId).orElseThrow(() -> new RuntimeException("유효하지 않은 방문 상세 기록")); + //todo memberAttraction 쿼리한방으로 줄일수도 있을것같다. + Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId)); + MemberAttraction memberAttraction = memberAttractionRepository.findById(memberAttractionId).orElseThrow(() -> new MemberAttractionNotFoundException(memberAttractionId)); return AttractionRecordDetailViewResponse.from(memberAttraction); } @@ -124,7 +127,7 @@ public AttractionRecordDetailViewResponse findOneVisitRecord(long memberId, long @Transactional public void updateVisitRecord(RecordDto dto, long memberId, long memberAttractionId) { //validation - MemberAttraction memberAttraction = memberAttractionRepository.findByIdAndMemberId(memberAttractionId, memberId).orElseThrow(() -> new RuntimeException("유효하지 않은 방문 상세 기록")); + MemberAttraction memberAttraction = memberAttractionRepository.findByIdAndMemberId(memberAttractionId, memberId).orElseThrow(() -> new MemberAttractionNotFoundException(memberAttractionId)); //update memberAttraction.updateVisitRecord(dto.region(), dto.dateTime(), dto.memo()); @@ -146,7 +149,7 @@ public void updateVisitRecord(RecordDto dto, long memberId, long memberAttractio public void deleteRecord(long memberId, long memberAttractionId) { //validation MemberAttraction memberAttraction = memberAttractionRepository.findByIdAndMemberId(memberAttractionId, memberId) - .orElseThrow(() -> new RuntimeException("유효하지 않은 방문 상세 기록")); + .orElseThrow(() -> new MemberAttractionNotFoundException(memberAttractionId)); List photos = photoRepository.findByMemberAttractionId(memberAttraction.getId()); photoRepository.deleteAll(photos); diff --git a/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java b/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java index 4424ca3..18f5f86 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/PhotoService.java @@ -16,7 +16,10 @@ import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; -import com.dnd.dndtravel.map.domain.Photo; +import com.dnd.dndtravel.map.exception.PhotoDeleteFailException; +import com.dnd.dndtravel.map.exception.PhotoEmptyException; +import com.dnd.dndtravel.map.exception.PhotoInvalidException; +import com.dnd.dndtravel.map.exception.PhotoUploadFailException; import lombok.RequiredArgsConstructor; @@ -31,7 +34,7 @@ public class PhotoService { public String upload(MultipartFile image) { if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) { - throw new RuntimeException("유효하지 않은 이미지"); + throw new PhotoEmptyException(); } validateFileExtension(image.getOriginalFilename()); return uploadImage(image); @@ -46,7 +49,7 @@ public void deleteBeforePhoto(List existingUrls) { try { amazonS3.deleteObject(bucketName, existingFileName); } catch (SdkClientException e) { - throw new RuntimeException("Failed to delete image from S3", e); + throw new PhotoDeleteFailException(existingFileName, e); } } } @@ -55,21 +58,21 @@ private String uploadImage(MultipartFile image) { try { return uploadImageToS3(image); } catch (IOException e) { - throw new RuntimeException("이미지 업로드 예외"); // custom ex (S3Exception) + throw new PhotoUploadFailException(image, e); } } private void validateFileExtension(String filename) { int lastDotIndex = filename.lastIndexOf("."); if (lastDotIndex == -1) { - throw new RuntimeException("파일 확장자가 없음"); + throw new PhotoInvalidException("파일 확장자가 없음"); } String extension = filename.substring(lastDotIndex + 1).toLowerCase(); List allowedExtentionList = Arrays.asList("jpg", "jpeg", "png"); if (!allowedExtentionList.contains(extension)) { - throw new RuntimeException("invalid file extension"); + throw new PhotoInvalidException("invalid file extension"); } } @@ -92,7 +95,7 @@ private String uploadImageToS3(MultipartFile image) throws IOException { //실제로 S3에 이미지 데이터를 넣는 부분이다. amazonS3.putObject(putObjectRequest); // put image to S3 } catch (SdkClientException e) { - throw new RuntimeException("Failed to upload image to S3", e); + throw new PhotoUploadFailException(s3FileName, e); } return amazonS3.getUrl(bucketName, s3FileName).toString(); From 24373b72a0affa8ef5a6c3cf44f26b9d68e29d35 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:56:33 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8#36=20-=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=9E=9C=EB=8D=A4=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 2 +- .../auth/service/MemberNameGenerator.java | 33 +++++++++++++++++++ .../member/service/MemberService.java | 5 ++- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/dnd/dndtravel/auth/service/MemberNameGenerator.java diff --git a/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java index 99dd550..95afa20 100644 --- a/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java +++ b/src/main/java/com/dnd/dndtravel/auth/controller/AuthController.java @@ -31,7 +31,7 @@ public ResponseEntity appleOAuthLogin(@RequestBody AppleLoginRequ AppleIdTokenPayload tokenPayload = appleOAuthService.get(appleLoginRequest.appleToken()); // apple에서 가져온 유저정보를 DB에 저장 - Member member = memberService.saveMember(tokenPayload.name(), tokenPayload.email(), appleLoginRequest.selectedColor()); + Member member = memberService.saveMember(tokenPayload.email(), appleLoginRequest.selectedColor()); // 클라이언트와 주고받을 user token(access , refresh) 생성 TokenResponse tokenResponse = jwtTokenService.generateTokens(member.getId()); diff --git a/src/main/java/com/dnd/dndtravel/auth/service/MemberNameGenerator.java b/src/main/java/com/dnd/dndtravel/auth/service/MemberNameGenerator.java new file mode 100644 index 0000000..5abc14a --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/service/MemberNameGenerator.java @@ -0,0 +1,33 @@ +package com.dnd.dndtravel.auth.service; + +import java.util.Random; + +import org.springframework.stereotype.Component; + +@Component +public class MemberNameGenerator { + private static final String[] ADJECTIVES = { + "가냘픈", "가엾은", "거센", "거친", "건조한", + "게으른", "고달픈", "귀여운", "그리운", "깨끗한", + "누런", "느린", "더러운", "덜된", "동그란", "뛰어난", + "무서운", "미친", "보람찬", "뽀얀", "비싼", "서툰","섣부른", + "성가신","수줍은","쏜살같은","못난", "못생긴", "무거운", "빠른", + "용감한", "행복한", "슬픈", "탐스러운", "한결같은","희망찬" + }; + + private static final String[] ANIMALS = { + "사자", "호랑이", "곰", "여우", "토끼", + "거북이", "고양이", "늑대", "강아지", "팬더", + "병아리", "망아지", "코끼리", "오징어", "바퀴벌레", + "거머리", "개미핥기", "돌고래", "순록", "올빼미", "박쥐", + "펭귄", "나무늘보", "오소리", "하이에나", "햄스터" + }; + + private final Random random = new Random(); + + public String generateRandomName() { + String adjective = ADJECTIVES[this.random.nextInt(ADJECTIVES.length)]; + String animal = ANIMALS[this.random.nextInt(ANIMALS.length)]; + return adjective + animal; + } +} diff --git a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java index 973a345..9051419 100644 --- a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java +++ b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java @@ -1,5 +1,6 @@ package com.dnd.dndtravel.member.service; +import com.dnd.dndtravel.auth.service.MemberNameGenerator; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -11,9 +12,11 @@ public class MemberService { private final MemberRepository memberRepository; + private final MemberNameGenerator memberNameGenerator; @Transactional - public Member saveMember(String name, String email, String selectedColor) { + public Member saveMember(String email, String selectedColor) { + String name = memberNameGenerator.generateRandomName(); return memberRepository.findByEmail(email) .orElseGet(() -> memberRepository.save(Member.of(name, email,selectedColor))); } From 0e5daf83165b25375e4752274596226e7db0e972 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:57:38 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=A8#36=20-=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20API=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 22 +++++++++++++++++++ .../member/service/MemberService.java | 8 +++++++ .../service/response/MyPageResponse.java | 6 +++++ 3 files changed, 36 insertions(+) create mode 100644 src/main/java/com/dnd/dndtravel/member/controller/MemberController.java create mode 100644 src/main/java/com/dnd/dndtravel/member/service/response/MyPageResponse.java diff --git a/src/main/java/com/dnd/dndtravel/member/controller/MemberController.java b/src/main/java/com/dnd/dndtravel/member/controller/MemberController.java new file mode 100644 index 0000000..f16dc1e --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/member/controller/MemberController.java @@ -0,0 +1,22 @@ +package com.dnd.dndtravel.member.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dnd.dndtravel.config.AuthenticationMember; +import com.dnd.dndtravel.member.service.MemberService; +import com.dnd.dndtravel.member.service.response.MyPageResponse; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +public class MemberController { + + private final MemberService memberService; + + @GetMapping("/mypages") + public MyPageResponse myPage(AuthenticationMember authenticationMember) { + return memberService.myPageInfo(authenticationMember.id()); + } +} diff --git a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java index 9051419..e73636d 100644 --- a/src/main/java/com/dnd/dndtravel/member/service/MemberService.java +++ b/src/main/java/com/dnd/dndtravel/member/service/MemberService.java @@ -3,6 +3,8 @@ import com.dnd.dndtravel.auth.service.MemberNameGenerator; import com.dnd.dndtravel.member.domain.Member; import com.dnd.dndtravel.member.repository.MemberRepository; +import com.dnd.dndtravel.member.service.response.MyPageResponse; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,4 +22,10 @@ public Member saveMember(String email, String selectedColor) { return memberRepository.findByEmail(email) .orElseGet(() -> memberRepository.save(Member.of(name, email,selectedColor))); } + + @Transactional(readOnly = true) + public MyPageResponse myPageInfo(long memberId) { + Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException()); + return new MyPageResponse(member.getName()); + } } diff --git a/src/main/java/com/dnd/dndtravel/member/service/response/MyPageResponse.java b/src/main/java/com/dnd/dndtravel/member/service/response/MyPageResponse.java new file mode 100644 index 0000000..ed4a4ca --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/member/service/response/MyPageResponse.java @@ -0,0 +1,6 @@ +package com.dnd.dndtravel.member.service.response; + +public record MyPageResponse( + String name +) { +} From 379086add78af7ef8b4e3c13e833266d461683ba Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:00:35 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=E2=9C=A8=20Runtime=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=A1=9C=EA=B7=B8=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=BC=EC=88=98=EC=9E=88=EA=B2=8C=20=EC=84=A4=EC=A0=95=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 --- .../com/dnd/dndtravel/common/CommonExceptionHandler.java | 6 ++++++ src/main/resources/application.yml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java b/src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java index 213f090..ca037c8 100644 --- a/src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java +++ b/src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java @@ -17,7 +17,11 @@ import com.dnd.dndtravel.map.exception.PhotoInvalidException; import com.dnd.dndtravel.map.exception.PhotoUploadFailException; import com.dnd.dndtravel.map.exception.RegionNotFoundException; + +import lombok.extern.slf4j.Slf4j; + //todo 예외클래스가 많아지면 해당클래스가 길어질것으로 예상, 개선필요해보이고 보안 때문에 상태코드별로 애매하게 동일한 메시지를 전달해주고, 스웨거 문서로 상세 오류를 전달해주는데 이 구조가 적절한건지 고민해봐야한다. +@Slf4j @RestControllerAdvice public class CommonExceptionHandler { @@ -111,6 +115,7 @@ public ResponseEntity runtimeException(PhotoInvalidException e) { @ExceptionHandler(RuntimeException.class) public ResponseEntity runtimeException(RuntimeException e) { + log.error("runtimeException = {}", e); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(INTERNAL_SERVER_ERROR_MESSAGE); @@ -118,6 +123,7 @@ public ResponseEntity runtimeException(RuntimeException e) { @ExceptionHandler(Exception.class) public ResponseEntity runtimeException(Exception e) { + log.error("exception = {}", e); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(INTERNAL_SERVER_ERROR_MESSAGE); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2da883f..1de3446 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,6 +14,10 @@ spring: hibernate: format_sql: true +logging: + level: + root: INFO # 로그 레벨 설정 + social-login: provider: apple: From 1153511fcdf2a29ad52eac74209392040c69f074 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:22:59 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=90=9B=20#39=20-=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=ED=8C=8C=EC=8B=B1=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 타입 캐스팅 예외와 BaseUrl 관련 오류가 있었음 --- src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java | 2 +- src/main/java/com/dnd/dndtravel/config/AuthResolver.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java b/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java index 3b43922..327a8a5 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java @@ -52,7 +52,7 @@ public Claims parseClaims(String splitHeader) { Jws claims; try { claims = Jwts.parser() - .verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(String.valueOf(this.secretKey)))) + .verifyWith(secretKey) .build() .parseSignedClaims(splitHeader); diff --git a/src/main/java/com/dnd/dndtravel/config/AuthResolver.java b/src/main/java/com/dnd/dndtravel/config/AuthResolver.java index 422ed8e..d4cb8f3 100644 --- a/src/main/java/com/dnd/dndtravel/config/AuthResolver.java +++ b/src/main/java/com/dnd/dndtravel/config/AuthResolver.java @@ -48,7 +48,8 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m Claims accessClaim = jwtProvider.parseClaims(splitHeaders[1]); //토큰에 심었던 user 식별자값 유효성 확인 - Member member = memberRepository.findById((Long)accessClaim.get(MEMBER_ID_CLAIM)) + Integer memberId = (Integer) accessClaim.get(MEMBER_ID_CLAIM); + Member member = memberRepository.findById(memberId.longValue()) .orElseThrow(() -> new RuntimeException("유효하지 않은 토큰 값")); // 컨트롤러 파라미터로 반환 From 5c364fde02899a0717a7ff98b969f30b8c1099a4 Mon Sep 17 00:00:00 2001 From: youngreal <59333182+youngreal@users.noreply.github.com> Date: Fri, 6 Sep 2024 00:01:01 +0900 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=90=9B=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=9D=BC=EB=B6=80=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EC=9D=BC=EB=B6=80=20record=ED=83=80=EC=9E=85=20class?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 로그인 성공시 RTR로 토큰을 관리하며 이미 존재하는 리프레시 토큰이 있을때도 새롭게 리프레시 토큰을 발급해주기위해 변경 * 빈 photo를 보냈을떄 서비스 layer에서 검증로직 추가 --- .../auth/service/JwtTokenService.java | 14 ++++++-- .../map/controller/request/RecordRequest.java | 33 +++++++++++++++---- .../dnd/dndtravel/map/service/MapService.java | 8 +++-- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java index 2e56e50..14f18bb 100644 --- a/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java +++ b/src/main/java/com/dnd/dndtravel/auth/service/JwtTokenService.java @@ -21,15 +21,23 @@ public class JwtTokenService { public TokenResponse generateTokens(Long memberId) { RefreshToken refreshToken = refreshTokenRepository.findByMemberId(memberId); + // 리프레시 토큰이 없는경우 if (refreshToken == null) { String newRefreshToken = jwtProvider.refreshToken(); refreshTokenRepository.save(RefreshToken.of(memberId, newRefreshToken)); // refreshToken은 DB에 저장 return new TokenResponse(jwtProvider.accessToken(memberId), newRefreshToken); - } else if (refreshToken.isExpire()) { + } + + // 리프레시 토큰이 만료됐으면 재발급 받으라고 멘트줌 + if (refreshToken.isExpire()) { return null; } - - return new TokenResponse(jwtProvider.accessToken(memberId), null); + + // 리프레시 토큰이 DB에 존재하고 유효한경우 + refreshTokenRepository.delete(refreshToken); + String newRefreshToken = jwtProvider.refreshToken(); + refreshTokenRepository.save(RefreshToken.of(refreshToken.getMemberId(), newRefreshToken)); + return new TokenResponse(jwtProvider.accessToken(memberId), newRefreshToken); } @Transactional diff --git a/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java b/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java index f8cd993..f0c8448 100644 --- a/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java +++ b/src/main/java/com/dnd/dndtravel/map/controller/request/RecordRequest.java @@ -16,24 +16,45 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -public record RecordRequest( +import lombok.Getter; +import lombok.Setter; + +/** + * record타입은 multipart로 받을때 Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of + * `*.request.RecordRequest` (although at least one Creator exists): + * no String-argument constructor/factory method to deserialize from String value 문제가 있었음 + * + */ +@Getter +@Setter +public class RecordRequest { @Schema(description = "지역 이름", requiredMode = REQUIRED) @RegionEnum(enumClass = RegionCondition.class) - String region, + String region; @Schema(description = "명소명", requiredMode = REQUIRED) @NotBlank(message = "명소명은 필수 입니다.") @Size(max = 10, message = "명소 이름은 10자 이내여야 합니다.") - String attractionName, + String attractionName; @Schema(description = "메모", requiredMode = NOT_REQUIRED) @Size(max = 25, message = "메모는 25자 이내여야 합니다.") - String memo, + String memo; @Schema(description = "방문날짜, ISO Date(yyyy-MM-dd) 형식으로 입력", requiredMode = NOT_REQUIRED) @NotNull(message = "날짜는 필수 입력 사항입니다.") - LocalDate localDate -) { + LocalDate localDate; + + public RecordRequest() { + } + + public RecordRequest(String region, String attractionName, String memo, LocalDate localDate) { + this.region = region; + this.attractionName = attractionName; + this.memo = memo; + this.localDate = localDate; + } + public RecordDto toDto(List photos) { return RecordDto.builder() .region(this.region) diff --git a/src/main/java/com/dnd/dndtravel/map/service/MapService.java b/src/main/java/com/dnd/dndtravel/map/service/MapService.java index 7caabce..b9f43bd 100644 --- a/src/main/java/com/dnd/dndtravel/map/service/MapService.java +++ b/src/main/java/com/dnd/dndtravel/map/service/MapService.java @@ -215,9 +215,11 @@ private void updateRegionVisitCount(Member member, Region region) { } private void savePhotos(List photos, MemberAttraction memberAttractionEntity) { - for (MultipartFile photo : photos) { - String imageUrl = photoService.upload(photo); - photoRepository.save(Photo.of(memberAttractionEntity, imageUrl)); + if (photos != null && !photos.isEmpty()) { + for (MultipartFile photo : photos) { + String imageUrl = photoService.upload(photo); + photoRepository.save(Photo.of(memberAttractionEntity, imageUrl)); + } } } }