Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ out/

### env ###
.env
local.env

### sql ###
*.sql
Expand Down
25 changes: 22 additions & 3 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Content-Type : application/json
{
"status": "http status" ,
"code": "custom status code",
"message" : "response message"
"message" : "response message",
"data": "data"
}
----
Expand Down Expand Up @@ -69,7 +69,7 @@ Content-Type: application/json
{
"status": "CREATED", // 예시
"code": "S002", //예시
"message" : "resource가 정상적으로 생성되었습니다."
"message" : "resource가 정상적으로 생성되었습니다.",
"data": null
}
----
Expand All @@ -82,7 +82,7 @@ Content-Type: application/json
{
"status": "OK", //예시
"code": "S001", //예시
"message" : "요청이 정상적으로 처리되었습니다."
"message" : "요청이 정상적으로 처리되었습니다.",
"data": {
"id": 123,
"name": "example"
Expand All @@ -99,3 +99,22 @@ Content-Type: application/json
| 공통 | 500 | INTERNAL_SERVER_ERROR | E500_001 | 서버 측에서 처리하지 못한 예외가 발생하면 모든 api 요청에 대해 공통적으로 반환됨.
|===

== 회원

=== **1. 이메일 중복 확인**

이메일 중복을 확인하는 api입니다.

==== Request
include::{snippets}/api/users/email/duplication/1/http-request.adoc[]

==== Request Query Parameter Fields
include::{snippets}/api/users/email/duplication/1/query-parameters.adoc[]

==== 성공 Response
include::{snippets}/api/users/email/duplication/1/http-response.adoc[]

==== Response Body Fields
include::{snippets}/api/users/email/duplication/1/response-fields.adoc[]

---
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.ftm.server.adapter.controller.user;

import com.ftm.server.adapter.dto.response.EmailDuplicationCheckResponse;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.domain.dto.query.FindByEmailQuery;
import com.ftm.server.domain.usecase.user.EmailDuplicationCheckUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class EmailDuplicationCheckController {

private final EmailDuplicationCheckUseCase emailDuplicationCheckUseCase;

@GetMapping("api/users/email/duplication")
public ResponseEntity<ApiResponse<EmailDuplicationCheckResponse>> emailDuplicationCheck(
@RequestParam(value = "email") String email) {
return ResponseEntity.status(HttpStatus.OK)
.body(
ApiResponse.success(
SuccessResponseCode.OK,
EmailDuplicationCheckResponse.from(
emailDuplicationCheckUseCase.emailDuplicationCheck(
FindByEmailQuery.of(email)))));
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ftm.server.adapter.dto.response;

import com.ftm.server.domain.dto.vo.EmailDuplicationVo;
import lombok.Data;

@Data
public class EmailDuplicationCheckResponse {

private final Boolean isDuplicated;

public static EmailDuplicationCheckResponse from(EmailDuplicationVo emailDuplicationVo) {
return new EmailDuplicationCheckResponse(emailDuplicationVo.getIsDuplicated());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
import com.ftm.server.entity.entities.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {}
public interface UserRepository extends JpaRepository<User, Long> {

Boolean existsByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ public class CustomException extends RuntimeException {

private final ErrorResponseCode errorResponseCode;

public CustomException(ErrorResponseCode errorResponseCode, String message) {
super(message); // 예외 메시지 설정
this.errorResponseCode = errorResponseCode;
}

public CustomException(ErrorResponseCode errorResponseCode) {
super(errorResponseCode.getMessage()); // 예외 메시지 설정
this.errorResponseCode = errorResponseCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.ftm.server.common.handler;

import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler({CustomException.class})
public ResponseEntity<ApiResponse> handleCustomException(CustomException e) {
log.error(
"[{}] code:{} / code message:{}",
e.getErrorResponseCode().name(),
e.getErrorResponseCode().getCode(),
e.getMessage());
return ResponseEntity.status(e.getErrorResponseCode().getHttpStatus())
.body(ApiResponse.fail(e.getErrorResponseCode()));
}

// 기타 처리되지 못한 exception
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handlingException(Exception e) {

log.error(
"[Exception] code : {} code message : {}",
ErrorResponseCode.UNKNOWN_SERVER_ERROR.getCode(),
e.getMessage());
return ResponseEntity.status(ErrorResponseCode.UNKNOWN_SERVER_ERROR.getHttpStatus())
.body(ApiResponse.fail(ErrorResponseCode.UNKNOWN_SERVER_ERROR));
}

// request body의 type이 잘못된 경우
@ExceptionHandler({
MethodArgumentNotValidException
.class, // json body (requestpart의 body, requestBody의 body)의 필드가 설정한 유효값을 만족시키지 않거나,
// 필수값이 누락됨.
HttpMessageNotReadableException
.class, // json body (requestpart의 body, requestBody의 body)의 필드 type이 잘못됨.
MissingServletRequestPartException.class, // required인 requestpart가 없음.
MissingServletRequestParameterException.class, // requried인 request param이 없음.
MethodArgumentTypeMismatchException.class // request parameter, pathVariable의 type이 잘못됨.
})
public ResponseEntity<ApiResponse> handleMissingServletRequestPartException(Exception e) {

log.error(
"[Exception] code : {} code message : {}",
ErrorResponseCode.INVALID_REQUEST_ARGUMENT.getCode(),
e.getMessage());
return ResponseEntity.status(ErrorResponseCode.INVALID_REQUEST_ARGUMENT.getHttpStatus())
.body(ApiResponse.fail(ErrorResponseCode.INVALID_REQUEST_ARGUMENT));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public enum ErrorResponseCode {

// 400번
INVALID_REQUEST_ARGUMENT(
HttpStatus.BAD_REQUEST, "E400_001", "클라이언트 요청이 잘못된 형식이거나, 필수 데이터가 누락되었습니다."),
HttpStatus.BAD_REQUEST, "E400_001", "클라이언트 요청값의 일부가 잘못된 형식이거나, 필수 데이터가 누락되었습니다."),

// 401번
NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "E401_001", "인증되지 않은 사용자입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.ftm.server.domain.dto.query;

import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import java.util.regex.Pattern;
import lombok.Getter;

@Getter
public class FindByEmailQuery {

private static final Pattern EMAIL_PATTERN =
Pattern.compile(
"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);

private final String email;

private FindByEmailQuery(String email) {
if (!isValidEmail(email)) {
throw new CustomException(
ErrorResponseCode.INVALID_REQUEST_ARGUMENT, "이메일 형식이 올바르지 않습니다.");
}
this.email = email;
}

public static FindByEmailQuery of(String email) {
return new FindByEmailQuery(email);
}

private boolean isValidEmail(String email) {
return EMAIL_PATTERN.matcher(email).matches();
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/ftm/server/domain/dto/vo/EmailDuplicationVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ftm.server.domain.dto.vo;

import lombok.Data;

@Data
public class EmailDuplicationVo {
private final Boolean isDuplicated;

public static EmailDuplicationVo of(Boolean isDuplicated) {
return new EmailDuplicationVo(isDuplicated);
}
}
Empty file.
20 changes: 20 additions & 0 deletions src/main/java/com/ftm/server/domain/service/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ftm.server.domain.service;

import com.ftm.server.adapter.gateway.repository.UserRepository;
import com.ftm.server.domain.dto.query.FindByEmailQuery;
import com.ftm.server.domain.dto.vo.EmailDuplicationVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

private final UserRepository userRepository;

public EmailDuplicationVo isEmailDuplicated(FindByEmailQuery query) {
return EmailDuplicationVo.of(userRepository.existsByEmail(query.getEmail()));
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ftm.server.domain.usecase.user;

import com.ftm.server.common.annotation.UseCase;
import com.ftm.server.domain.dto.query.FindByEmailQuery;
import com.ftm.server.domain.dto.vo.EmailDuplicationVo;
import com.ftm.server.domain.service.UserService;
import lombok.RequiredArgsConstructor;

@UseCase
@RequiredArgsConstructor
public class EmailDuplicationCheckUseCase {

private final UserService userService;

public EmailDuplicationVo emailDuplicationCheck(FindByEmailQuery query) {
return userService.isEmailDuplicated(query);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class SecurityConfig {
"https://dev-api.fittheman.site", // 개발 환경 서버 도메인
"https://fittheman.site"); // 개발 환경 클라이언트 도메인

private static final String[] ANONYMOUS_MATCHERS = {"/docs/**", "/api/users/email/duplication"};

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
Expand Down Expand Up @@ -74,7 +76,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
authorize -> {
authorize
// 정적 리소스 경로 허용
.requestMatchers("/docs/**")
.requestMatchers(ANONYMOUS_MATCHERS)
.permitAll();

// TODO: 요청 허용 특정 API 추가 (회원가입, 로그인 등)
Expand Down
61 changes: 61 additions & 0 deletions src/test/java/com/ftm/server/BaseTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.ftm.server;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders;

import com.fasterxml.jackson.databind.ObjectMapper;
import groovy.util.logging.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.operation.preprocess.HeadersModifyingOperationPreprocessor;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@Slf4j
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@SpringBootTest(classes = {ServerApplication.class})
@ActiveProfiles("test")
@ExtendWith({RestDocumentationExtension.class})
@AutoConfigureTestDatabase(
replace = AutoConfigureTestDatabase.Replace.NONE) // 테스트 시 내장된 인메모리 DB를 사용하지 않는다는 설정
@TestPropertySource(locations = "file:.env")
public class BaseTest {

@Autowired protected MockMvc mockMvc;

protected final ObjectMapper mapper = new ObjectMapper();

@BeforeEach
void setup(WebApplicationContext context, RestDocumentationContextProvider restDocumentation) {
this.mockMvc =
MockMvcBuilders.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity()) // secrutiy filter 적용
.apply(documentationConfiguration(restDocumentation))
.build();
}

// 블필요한 header 제거 함수
protected HeadersModifyingOperationPreprocessor getModifiedHeader() {
return modifyHeaders()
.remove("X-Content-Type-Options")
.remove("X-XSS-Protection")
.remove("Cache-Control")
.remove("Pragma")
.remove("Expires")
.remove("Content-Length")
.remove("X-Frame-Options")
.remove("Vary");
}
}
Loading