Skip to content

Commit

Permalink
Merge branch 'main' into feat/#34
Browse files Browse the repository at this point in the history
  • Loading branch information
youngreal authored Sep 4, 2024
2 parents e55f3e9 + 8c24d83 commit ed11b69
Show file tree
Hide file tree
Showing 21 changed files with 418 additions and 72 deletions.
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 @@ -17,7 +17,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

0 comments on commit ed11b69

Please sign in to comment.