Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#31] Swaager 추가 #33

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<TokenResponse> appleOAuthLogin(@RequestBody AppleLoginRequest appleLoginRequest) {
// 클라이언트에서 준 code 값으로 apple의 IdToken Payload를 얻어온다
Expand Down
Original file line number Diff line number Diff line change
@@ -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
){
}
Original file line number Diff line number Diff line change
@@ -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<? extends java.lang.Enum<?>> enumClass();
}
Original file line number Diff line number Diff line change
@@ -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<ColorValidation, String> {

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));
}
}
Original file line number Diff line number Diff line change
@@ -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<TokenResponse> 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
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@

@RequiredArgsConstructor
@Component
@Slf4j
public class AppleOAuthService {
private final AppleClient appleClient;
private final AppleProperties appleProperties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/dnd/dndtravel/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("MAPDDANG API")
.description("맵땅 앱 관련 API")
.version("1.0.0"));

}
}
40 changes: 19 additions & 21 deletions src/main/java/com/dnd/dndtravel/map/controller/MapController.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<MultipartFile> photos,
@RequestPart("recordRequest") RecordRequest recordRequest,
AuthenticationMember authenticationMember
AuthenticationMember authenticationMember,
@PhotoValidation @RequestPart(value = "photos", required = false) List<MultipartFile> photos,
@RequestPart("recordRequest") RecordRequest recordRequest
) {
mapService.recordAttraction(recordRequest.toDto(photos), authenticationMember.id());
}
Expand All @@ -53,38 +51,38 @@ public void memo(
//서버에서 클라에 마지막 게시글의 ID를 줘야함
@GetMapping("/maps/history")
public List<AttractionRecordResponse> 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);
}

// 기록 단건 조회
@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<MultipartFile> photos,
@PhotoValidation @RequestPart(value = "photos", required = false) List<MultipartFile> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
) {
Expand Down
Loading
Loading