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/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/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/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/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/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; 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이라는 상수로 고정 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)); + } }