diff --git a/build.gradle b/build.gradle index 3e7970b..c9dc350 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.3' + id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' } -group = 'com.myhome' +group = 'com.pinhouse' version = '0.0.1-SNAPSHOT' java { @@ -38,8 +38,14 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' // 시큐리티 -// implementation 'org.springframework.boot:spring-boot-starter-security' -// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.springframework.boot:spring-boot-starter-webflux' diff --git a/pinhouse_docker/docker-compose-dev.yml b/pinhouse_docker/docker-compose-dev.yml new file mode 100644 index 0000000..2661de1 --- /dev/null +++ b/pinhouse_docker/docker-compose-dev.yml @@ -0,0 +1,21 @@ +services: + + mysql: + image: mysql:8.0 + container_name: pinhouse-mysql + volumes: + - ./mysql/conf.d/my.cnf:/etc/mysql/conf.d/my.cnf + - ./mysql/data:/var/lib/mysql + ports: + - '3306:3306' + environment: + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + + redis: + image: redis:7.2.5 + container_name: pinhouse-redis + ports: + - '6379:6379' diff --git a/pinhouse_docker/mysql/conf.d/my.cnf b/pinhouse_docker/mysql/conf.d/my.cnf new file mode 100644 index 0000000..cf3cd92 --- /dev/null +++ b/pinhouse_docker/mysql/conf.d/my.cnf @@ -0,0 +1,10 @@ +[client] +default-character-set = utf8mb4 + +[mysql] +default-character-set = utf8mb4 + +[mysqld] +character-set-client-handshake = FALSE +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci diff --git a/src/main/java/com/myhome/server/core/response/response/ApiResponse.java b/src/main/java/com/myhome/server/core/response/response/ApiResponse.java deleted file mode 100644 index 7f2c59c..0000000 --- a/src/main/java/com/myhome/server/core/response/response/ApiResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.myhome.server.core.response.response; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.micrometer.common.lang.Nullable; -import org.springframework.http.HttpStatus; - -public record ApiResponse( - @JsonIgnore - HttpStatus httpStatus, - boolean success, - @Nullable T data, - @Nullable ExceptionDto error -) { - - public static ApiResponse ok(@Nullable final T data) { - return new ApiResponse<>(HttpStatus.OK, true, data, null); - } - - public static ApiResponse created(@Nullable final T data) { - return new ApiResponse<>(HttpStatus.CREATED, true, data, null); - } - - public static ApiResponse fail(final CustomException e) { - return new ApiResponse<>(e.getErrorCode().getHttpStatus(), false, null, ExceptionDto.of(e.getErrorCode(), e.getErrorMessage())); - } -} diff --git a/src/main/java/com/myhome/server/core/response/response/CustomException.java b/src/main/java/com/myhome/server/core/response/response/CustomException.java deleted file mode 100644 index ca9d1c6..0000000 --- a/src/main/java/com/myhome/server/core/response/response/CustomException.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.myhome.server.core.response.response; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class CustomException extends RuntimeException{ - private final ErrorCode errorCode; - private final String errorMessage; - -} diff --git a/src/main/java/com/myhome/server/core/response/response/ErrorCode.java b/src/main/java/com/myhome/server/core/response/response/ErrorCode.java deleted file mode 100644 index 2d0c4ea..0000000 --- a/src/main/java/com/myhome/server/core/response/response/ErrorCode.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.myhome.server.core.response.response; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum ErrorCode { - - // Test Error - TEST_ERROR(100, HttpStatus.BAD_REQUEST, "테스트 에러입니다."), - // 400 Bad Request - BAD_REQUEST(400, HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), - // 401 SC_UNAUTHORIZED - SC_UNAUTHORIZED(40100, HttpStatus.UNAUTHORIZED, "로그인이 필요합니다"), - // 403 Bad Reques - FORBIDDEN(40300, HttpStatus.FORBIDDEN, "접속 권한이 없습니다."), - // 404 Not Found - NOT_FOUND_END_POINT(404, HttpStatus.NOT_FOUND, "요청한 대상이 존재하지 않습니다."), - // 500 Internal Server Error - INTERNAL_SERVER_ERROR(500, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."), - - /// 유저 관련 - NOT_USER(1000, HttpStatus.NOT_FOUND, "해당하는 유저가 존재하지 않습니다."), - NOT_CONSENT(1010, HttpStatus.BAD_REQUEST, "유저 정보 동의가 체크되지 않았습니다"), - FIRST_LOGIN(1200, HttpStatus.NOT_FOUND, "최초 로그인유저이기에 추가정보기입이 필요합니다."), - - /// 공고 관련 - NOT_NOTICE(2000, HttpStatus.NOT_FOUND, "해당하는 공고가 존재하지 않습니다"), - - - /// 파라미터 관련 - BAD_PARAMETER(10000, HttpStatus.BAD_REQUEST, "요청 파라미터에 문제가 존재합니다."), - ; - - private final Integer code; - private final HttpStatus httpStatus; - private final String message; - - - /// 에러코드로 변환시키기 - public static ErrorCode fromMessage(String message) { - for (ErrorCode errorCode : ErrorCode.values()) { - if (errorCode.getMessage().equals(message)) { - return errorCode; - } - } - throw new IllegalArgumentException("해당 message를 가진 ErrorCode가 존재하지 않습니다: " + message); - } -} diff --git a/src/main/java/com/myhome/server/core/response/response/ExceptionDto.java b/src/main/java/com/myhome/server/core/response/response/ExceptionDto.java deleted file mode 100644 index 9185d0e..0000000 --- a/src/main/java/com/myhome/server/core/response/response/ExceptionDto.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.myhome.server.core.response.response; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.antlr.v4.runtime.misc.NotNull; - -@Getter -@RequiredArgsConstructor -public class ExceptionDto { - @NotNull - private final Integer code; - - @NotNull - private final String message; - - @NotNull - private final String errorMessage; - - public ExceptionDto(ErrorCode errorCode, String errorMessage) { - this.code = errorCode.getCode(); - this.message = errorCode.getMessage(); - this.errorMessage = errorMessage; - } - - public static ExceptionDto of(ErrorCode errorCode, String errorMessage) { - return new ExceptionDto(errorCode, errorMessage); - } - - public static ExceptionDto of(ErrorCode errorCode) { - return new ExceptionDto(errorCode, null); - } - -} diff --git a/src/main/java/com/myhome/server/platform/domain/notification/Notification.java b/src/main/java/com/myhome/server/platform/domain/notification/Notification.java deleted file mode 100644 index 9541bda..0000000 --- a/src/main/java/com/myhome/server/platform/domain/notification/Notification.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.myhome.server.platform.domain.notification; - -import com.myhome.server.platform.domain.BaseDomain; - -public class Notification extends BaseDomain { -} diff --git a/src/main/java/com/myhome/server/ServerApplication.java b/src/main/java/com/pinHouse/server/ServerApplication.java similarity index 70% rename from src/main/java/com/myhome/server/ServerApplication.java rename to src/main/java/com/pinHouse/server/ServerApplication.java index 11b098d..d99896d 100644 --- a/src/main/java/com/myhome/server/ServerApplication.java +++ b/src/main/java/com/pinHouse/server/ServerApplication.java @@ -1,9 +1,11 @@ -package com.myhome.server; +package com.pinHouse.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class ServerApplication { public static void main(String[] args) { diff --git a/src/main/java/com/myhome/server/core/config/MongoConfig.java b/src/main/java/com/pinHouse/server/core/config/MongoConfig.java similarity index 96% rename from src/main/java/com/myhome/server/core/config/MongoConfig.java rename to src/main/java/com/pinHouse/server/core/config/MongoConfig.java index 43974cd..ff5c6a5 100644 --- a/src/main/java/com/myhome/server/core/config/MongoConfig.java +++ b/src/main/java/com/pinHouse/server/core/config/MongoConfig.java @@ -1,4 +1,4 @@ -package com.myhome.server.core.config; +package com.pinHouse.server.core.config; import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; diff --git a/src/main/java/com/myhome/server/core/config/SwaggerConfig.java b/src/main/java/com/pinHouse/server/core/config/SwaggerConfig.java similarity index 96% rename from src/main/java/com/myhome/server/core/config/SwaggerConfig.java rename to src/main/java/com/pinHouse/server/core/config/SwaggerConfig.java index 859d96a..1aca4f0 100644 --- a/src/main/java/com/myhome/server/core/config/SwaggerConfig.java +++ b/src/main/java/com/pinHouse/server/core/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package com.myhome.server.core.config; +package com.pinHouse.server.core.config; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; diff --git a/src/main/java/com/myhome/server/core/config/WebClientConfig.java b/src/main/java/com/pinHouse/server/core/config/WebClientConfig.java similarity index 89% rename from src/main/java/com/myhome/server/core/config/WebClientConfig.java rename to src/main/java/com/pinHouse/server/core/config/WebClientConfig.java index edaaf7e..d06c391 100644 --- a/src/main/java/com/myhome/server/core/config/WebClientConfig.java +++ b/src/main/java/com/pinHouse/server/core/config/WebClientConfig.java @@ -1,4 +1,4 @@ -package com.myhome.server.core.config; +package com.pinHouse.server.core.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/pinHouse/server/core/exception/GlobalExceptionHandler.java b/src/main/java/com/pinHouse/server/core/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ed83056 --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/exception/GlobalExceptionHandler.java @@ -0,0 +1,105 @@ +package com.pinHouse.server.core.exception; + +import com.pinHouse.server.core.response.response.ApiResponse; +import com.pinHouse.server.core.response.response.CustomException; +import com.pinHouse.server.core.response.response.ErrorCode; +import com.pinHouse.server.core.response.response.FieldErrorResponse; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import java.util.List; +import java.util.NoSuchElementException; + +/** + * 전역 예외 처리 핸들러 + * - @Hidden 은 스웨거에서 인식 되지 않도록 에러 수정용 + */ + +@Hidden +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /// 공통 처리 메서드 + private ApiResponse handleCustomException(CustomException customException, HttpServletRequest request) { + String username = request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : "anonymous"; + ErrorCode errorCode = customException.getErrorCode(); + + log.info("[EXCEPTION] 사용자: {}, 메서드: {}, URI: {}, 예외: {}", + username, request.getMethod(), request.getRequestURI(), errorCode.getMessage()); + + return ApiResponse.fail(customException); + } + + + /// 예외 처리 + // 최하위 예외처리 + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ApiResponse handleException(Exception e, HttpServletRequest request) { + + /// 로그 발생 + log.error(e.getMessage(), e); + + /// 500 예외 코드 검색 + ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; + + /// 해당 예외 코드로 예외 처리 + CustomException exception = new CustomException(errorCode, null); + + return handleCustomException(exception, request); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ + IllegalStateException.class, IllegalArgumentException.class}) + public ApiResponse handleIllegalStateException(Exception e, HttpServletRequest request) { + + /// 메세지 바탕으로 예외 코드 검색 + ErrorCode errorCode = ErrorCode.fromMessage(e.getMessage()); + + /// 해당 예외 코드로 예외 처리 + CustomException exception = new CustomException(errorCode, null); + + return handleCustomException(exception, request); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler({NoSuchElementException.class, NoResourceFoundException.class}) + public ApiResponse handleNoSuchException(Exception e, HttpServletRequest request) { + + /// 메세지 바탕으로 예외 코드 검색 + ErrorCode errorCode = ErrorCode.fromMessage(e.getMessage()); + + /// 해당 예외 코드로 예외 처리 + CustomException exception = new CustomException(errorCode, null); + + return handleCustomException(exception, request); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ApiResponse handleValidationExceptions(MethodArgumentNotValidException e, HttpServletRequest request) { + + /// 파라미터용 예외 코드 + ErrorCode errorCode = ErrorCode.BAD_PARAMETER; + + /// BindingResult 바탕으로 필드에러 List 생성 + List errors = e.getBindingResult().getFieldErrors() + .stream() + .map(error -> FieldErrorResponse.of(error.getField(), error.getDefaultMessage())) + .toList(); + + CustomException exception = new CustomException(errorCode, errors); + + return handleCustomException(exception, request); + } + +} diff --git a/src/main/java/com/pinHouse/server/core/response/response/ApiResponse.java b/src/main/java/com/pinHouse/server/core/response/response/ApiResponse.java new file mode 100644 index 0000000..f9f5ead --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/response/response/ApiResponse.java @@ -0,0 +1,43 @@ +package com.pinHouse.server.core.response.response; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.micrometer.common.lang.Nullable; +import org.springframework.http.HttpStatus; + +import java.util.List; + +/** + * API 응답을 표준화하기 위한 레코드 클래스입니다. + */ + +public record ApiResponse( + @JsonIgnore + HttpStatus httpStatus, + boolean success, + Integer code, + String message, + @Nullable T data, + @Nullable List error +) { + + // 성공 응답 생성 (200 OK) + public static ApiResponse ok(@Nullable final T data) { + return new ApiResponse<>(HttpStatus.OK, true, HttpStatus.OK.value(), "호출이 성공적으로 완료되었습니다.", data, null); + } + + // 생성 성공 응답 (201 Created) + public static ApiResponse created() { + return new ApiResponse<>(HttpStatus.CREATED, true, HttpStatus.CREATED.value(), "성공적으로 생성되었습니다.", null, null); + } + + // 삭제 성공 응답 (204 No Content) + public static ApiResponse deleted() { + return new ApiResponse<>(HttpStatus.NO_CONTENT, true, HttpStatus.NO_CONTENT.value(), "성공적으로 삭제 되었습니다.", null, null); + } + + // 실패 응답 생성 + public static ApiResponse fail(final CustomException e) { + return new ApiResponse<>(e.getErrorCode().getHttpStatus(), false, e.getErrorCode().getCode(), e.getErrorCode().getMessage(), null, e.getFieldErrorResponses()); + } + +} diff --git a/src/main/java/com/pinHouse/server/core/response/response/CustomException.java b/src/main/java/com/pinHouse/server/core/response/response/CustomException.java new file mode 100644 index 0000000..4b05a65 --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/response/response/CustomException.java @@ -0,0 +1,22 @@ +package com.pinHouse.server.core.response.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import java.util.List; + + +/** + * 응답을 하는 커스텀 예외 클래스입니다. + * ErrorCode와 필드별 에러 정보를 함께 담아, 일관된 에러 응답을 제공합니다. + */ + +@Getter +@RequiredArgsConstructor +public class CustomException extends RuntimeException{ + + private final ErrorCode errorCode; + + private final List fieldErrorResponses; + + +} diff --git a/src/main/java/com/pinHouse/server/core/response/response/ErrorCode.java b/src/main/java/com/pinHouse/server/core/response/response/ErrorCode.java new file mode 100644 index 0000000..2a96243 --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/response/response/ErrorCode.java @@ -0,0 +1,117 @@ +package com.pinHouse.server.core.response.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.util.Arrays; + +/** + * 애플리케이션 전역에서 사용하는 에러 코드 Enum입니다. + * 각 에러는 고유 코드, HTTP 상태, 메시지를 포함합니다. + * + * 0~999 : 공통 에러 + * 1000~1999 : 유저 관련 에러 + * 2000~2999 : 영화관 관련 에러 + * 3000~3999 : 리뷰 관련 에러 + * 10000 이상 : 기타 파라미터 등 + */ +@Getter +@AllArgsConstructor +public enum ErrorCode { + + + // ======================== + // 0~1000 : 공통 및 보안 에러 + // ======================== + + // ======================== + // 400 Bad Request + // ======================== + BAD_REQUEST(400_000, HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + INVALID_FILE_FORMAT(400_001, HttpStatus.BAD_REQUEST, "업로드된 파일 형식이 올바르지 않습니다."), + INVALID_INPUT(400_002, HttpStatus.BAD_REQUEST, "입력값이 올바르지 않습니다."), + NULL_VALUE(400_003, HttpStatus.BAD_REQUEST, "Null 값이 들어왔습니다."), + TEST_ERROR(400_004, HttpStatus.BAD_REQUEST, "테스트 에러입니다."), + + + // ======================== + // 401 Unauthorized + // ======================== + TOKEN_EXPIRED(401_000, HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), + TOKEN_INVALID(401_001, HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), + TOKEN_NOT_FOUND(401_002, HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다."), + TOKEN_UNSUPPORTED(401_003, HttpStatus.UNAUTHORIZED, "지원하지 않는 토큰 형식입니다."), + INVALID_CREDENTIALS(401_004, HttpStatus.UNAUTHORIZED, "인증 정보가 올바르지 않습니다."), + INVALID_REFRESH_TOKEN(401_005, HttpStatus.UNAUTHORIZED, "재발급 토큰이 유효하지 않습니다."), + INVALID_ACCESS_TOKEN(401_006, HttpStatus.UNAUTHORIZED, "접근 토큰이 유효하지 않습니다."), + INVALID_TOKEN(401_007, HttpStatus.UNAUTHORIZED, "토큰이 생성되지 않았습니다."), + INVALID_LOGIN(401_008, HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."), + REFRESH_TOKEN_NOT_FOUND(401_011, HttpStatus.UNAUTHORIZED, "저장된 리프레시 토큰이 존재하지 않습니다."), + REFRESH_TOKEN_MISMATCH(401_009, HttpStatus.UNAUTHORIZED, "저장된 리프레시 토큰과 일치하지 않습니다."), + EXPIRED_REFRESH_TOKEN(401_010, HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다."), + TOKEN_NOT_FOUND_COOKIE(401_011, HttpStatus.UNAUTHORIZED, "쿠키에 리프레시 토큰이 존재하지 않습니다."), + UNSUPPORTED_SOCIAL_LOGIN(401_012, HttpStatus.UNAUTHORIZED, "지원하지 않는 소셜 로그인 방식입니다."), + + + + + // ======================== + // 403 Forbidden + // ======================== + FORBIDDEN(403_000, HttpStatus.FORBIDDEN, "접속 권한이 없습니다."), + ACCESS_DENY(403_001, HttpStatus.FORBIDDEN, "접근이 거부되었습니다."), + UNAUTHORIZED_POST_ACCESS(403_002, HttpStatus.FORBIDDEN, "해당 게시글에 접근할 권한이 없습니다."), + + + // ======================== + // 404 Not Found + // ======================== + NOT_FOUND_END_POINT(404_000, HttpStatus.NOT_FOUND, "요청한 대상이 존재하지 않습니다."), + USER_NOT_FOUND(404_001, HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + USER_NOT_FOUND_IN_COOKIE(404_002, HttpStatus.NOT_FOUND, "쿠키에서 사용자 정보를 찾을 수 없습니다."), + POST_NOT_FOUND(404_003, HttpStatus.NOT_FOUND, "요청한 게시글을 찾을 수 없습니다."), + POST_TYPE_NOT_FOUND(404_004, HttpStatus.NOT_FOUND, "게시글 타입을 찾을 수 없습니다."), + COMMENT_NOT_FOUND(404_005, HttpStatus.NOT_FOUND, "요청한 댓글을 찾을 수 없습니다."), + PRODUCT_NOT_FOUND(404_006, HttpStatus.NOT_FOUND, "해당 상품을 찾을 수 없습니다."), + NOT_NOTICE(404_007,HttpStatus.NOT_FOUND,"해당 공고를 찾을 수 없습니다"), + + // ======================== + // 409 Conflict + // ======================== + DUPLICATE_EMAIL(409_001, HttpStatus.CONFLICT, "이미 사용 중인 이메일입니다."), + BOOKMARK_NOT_OWN_USER(409_002, HttpStatus.CONFLICT, "유저가 추가한 북마크가 아닙니다."), + BOOKMARK_ALREADY(409_003,HttpStatus.CONFLICT,"이미 해당 상품에 북마크를 등록했습니다"), + + + // ======================== + // 500 Internal Server Error + // ======================== + INTERNAL_SERVER_ERROR(500_000, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."), + BAD_PARAMETER(999, HttpStatus.BAD_REQUEST, "요청 파라미터에 문제가 존재합니다."),; + + + /** 에러 코드 (고유값) */ + private final Integer code; + + /** HTTP 상태 코드 */ + private final HttpStatus httpStatus; + + /** 에러 메시지 */ + private final String message; + + + /** + * 메시지를 바탕으로 ErrorCode를 반환합니다. + * 동일한 메시지가 여러 ErrorCode에 할당된 경우, 첫 번째로 일치하는 ErrorCode를 반환합니다. + * + * @param message 에러 메시지 + * @return 일치하는 ErrorCode + */ + public static ErrorCode fromMessage(String message) { + return Arrays.stream(values()) + .filter(code -> code.message.equalsIgnoreCase(message)) + .findFirst() + .orElse(ErrorCode.INTERNAL_SERVER_ERROR); // 기본 에러 처리 + } +} diff --git a/src/main/java/com/pinHouse/server/core/response/response/FieldErrorResponse.java b/src/main/java/com/pinHouse/server/core/response/response/FieldErrorResponse.java new file mode 100644 index 0000000..fb9a77e --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/response/response/FieldErrorResponse.java @@ -0,0 +1,16 @@ +package com.pinHouse.server.core.response.response; + +/** + * 파라미터 오류에 대한 예외처리 입니다. + * @param field 에러가 발생한 필드명 + * @param message 해당 필드의 에러 메시지 + */ + +public record FieldErrorResponse( + String field, + String message) { + + public static FieldErrorResponse of(String field, String message) { + return new FieldErrorResponse(field, message); + } +} diff --git a/src/main/java/com/myhome/server/core/response/response/pageable/PageRequest.java b/src/main/java/com/pinHouse/server/core/response/response/pageable/PageRequest.java similarity index 86% rename from src/main/java/com/myhome/server/core/response/response/pageable/PageRequest.java rename to src/main/java/com/pinHouse/server/core/response/response/pageable/PageRequest.java index 0a408ed..d77ea6e 100644 --- a/src/main/java/com/myhome/server/core/response/response/pageable/PageRequest.java +++ b/src/main/java/com/pinHouse/server/core/response/response/pageable/PageRequest.java @@ -1,4 +1,4 @@ -package com.myhome.server.core.response.response.pageable; +package com.pinHouse.server.core.response.response.pageable; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/myhome/server/core/response/response/pageable/PageResponse.java b/src/main/java/com/pinHouse/server/core/response/response/pageable/PageResponse.java similarity index 95% rename from src/main/java/com/myhome/server/core/response/response/pageable/PageResponse.java rename to src/main/java/com/pinHouse/server/core/response/response/pageable/PageResponse.java index 1b0bf43..eaf970a 100644 --- a/src/main/java/com/myhome/server/core/response/response/pageable/PageResponse.java +++ b/src/main/java/com/pinHouse/server/core/response/response/pageable/PageResponse.java @@ -1,4 +1,4 @@ -package com.myhome.server.core.response.response.pageable; +package com.pinHouse.server.core.response.response.pageable; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/com/pinHouse/server/core/util/BirthDayUtil.java b/src/main/java/com/pinHouse/server/core/util/BirthDayUtil.java new file mode 100644 index 0000000..58cb93d --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/util/BirthDayUtil.java @@ -0,0 +1,28 @@ +package com.pinHouse.server.core.util; + +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +@Component +public class BirthDayUtil { + + /** + * 생년월일 합치는 유틸 클래스 + * @param year 년도 + * @param monthDay 월일 + */ + public static LocalDate parseBirthday(String year, String monthDay) { + try { + String fullDate = year + "-" + monthDay; + return LocalDate.parse(fullDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("잘못된 생년월일 형식입니다. year=" + year + ", monthDay=" + monthDay); + } + } + + + +} diff --git a/src/main/java/com/myhome/server/platform/adapter/in/web/DepositApi.java b/src/main/java/com/pinHouse/server/platform/adapter/in/web/DepositApi.java similarity index 74% rename from src/main/java/com/myhome/server/platform/adapter/in/web/DepositApi.java rename to src/main/java/com/pinHouse/server/platform/adapter/in/web/DepositApi.java index 6fac45a..baf0234 100644 --- a/src/main/java/com/myhome/server/platform/adapter/in/web/DepositApi.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/in/web/DepositApi.java @@ -1,8 +1,8 @@ -package com.myhome.server.platform.adapter.in.web; +package com.pinHouse.server.platform.adapter.in.web; -import com.myhome.server.core.response.response.ApiResponse; -import com.myhome.server.platform.adapter.in.web.dto.response.NoticeSupplyDTO; -import com.myhome.server.platform.application.in.NoticeUseCase; +import com.pinHouse.server.core.response.response.ApiResponse; +import com.pinHouse.server.platform.adapter.in.web.dto.response.NoticeSupplyDTO; +import com.pinHouse.server.platform.application.in.NoticeUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/myhome/server/platform/adapter/in/web/DistanceApi.java b/src/main/java/com/pinHouse/server/platform/adapter/in/web/DistanceApi.java similarity index 83% rename from src/main/java/com/myhome/server/platform/adapter/in/web/DistanceApi.java rename to src/main/java/com/pinHouse/server/platform/adapter/in/web/DistanceApi.java index c7dfb56..d37b278 100644 --- a/src/main/java/com/myhome/server/platform/adapter/in/web/DistanceApi.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/in/web/DistanceApi.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.adapter.in.web; +package com.pinHouse.server.platform.adapter.in.web; -import com.myhome.server.platform.application.out.distance.DistancePort; +import com.pinHouse.server.platform.application.out.distance.DistancePort; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/myhome/server/platform/adapter/in/web/NoticeApi.java b/src/main/java/com/pinHouse/server/platform/adapter/in/web/NoticeApi.java similarity index 73% rename from src/main/java/com/myhome/server/platform/adapter/in/web/NoticeApi.java rename to src/main/java/com/pinHouse/server/platform/adapter/in/web/NoticeApi.java index 6da3a86..9c6602f 100644 --- a/src/main/java/com/myhome/server/platform/adapter/in/web/NoticeApi.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/in/web/NoticeApi.java @@ -1,11 +1,11 @@ -package com.myhome.server.platform.adapter.in.web; - -import com.myhome.server.core.response.response.ApiResponse; -import com.myhome.server.core.response.response.pageable.PageRequest; -import com.myhome.server.core.response.response.pageable.PageResponse; -import com.myhome.server.platform.adapter.in.web.dto.response.NoticeDTO; -import com.myhome.server.platform.adapter.in.web.swagger.NoticeApiSpec; -import com.myhome.server.platform.application.in.NoticeUseCase; +package com.pinHouse.server.platform.adapter.in.web; + +import com.pinHouse.server.core.response.response.ApiResponse; +import com.pinHouse.server.core.response.response.pageable.PageRequest; +import com.pinHouse.server.core.response.response.pageable.PageResponse; +import com.pinHouse.server.platform.adapter.in.web.dto.response.NoticeDTO; +import com.pinHouse.server.platform.adapter.in.web.swagger.NoticeApiSpec; +import com.pinHouse.server.platform.application.in.NoticeUseCase; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/NoticeDTO.java b/src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/NoticeDTO.java similarity index 96% rename from src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/NoticeDTO.java rename to src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/NoticeDTO.java index 115f752..e35d0b0 100644 --- a/src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/NoticeDTO.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/NoticeDTO.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.adapter.in.web.dto.response; +package com.pinHouse.server.platform.adapter.in.web.dto.response; -import com.myhome.server.platform.domain.notice.Notice; +import com.pinHouse.server.platform.domain.notice.Notice; import lombok.Builder; import java.util.List; diff --git a/src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/NoticeSupplyDTO.java b/src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/NoticeSupplyDTO.java similarity index 93% rename from src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/NoticeSupplyDTO.java rename to src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/NoticeSupplyDTO.java index 1a550a8..9331e7f 100644 --- a/src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/NoticeSupplyDTO.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/NoticeSupplyDTO.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.adapter.in.web.dto.response; +package com.pinHouse.server.platform.adapter.in.web.dto.response; -import com.myhome.server.platform.domain.notice.NoticeSupplyInfo; +import com.pinHouse.server.platform.domain.notice.NoticeSupplyInfo; import lombok.Builder; public record NoticeSupplyDTO() { diff --git a/src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/OdsayResponse.java b/src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/OdsayResponse.java similarity index 97% rename from src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/OdsayResponse.java rename to src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/OdsayResponse.java index becd6de..b83a19d 100644 --- a/src/main/java/com/myhome/server/platform/adapter/in/web/dto/response/OdsayResponse.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/in/web/dto/response/OdsayResponse.java @@ -1,4 +1,4 @@ -package com.myhome.server.platform.adapter.in.web.dto.response; +package com.pinHouse.server.platform.adapter.in.web.dto.response; import java.util.List; diff --git a/src/main/java/com/myhome/server/platform/adapter/in/web/swagger/NoticeApiSpec.java b/src/main/java/com/pinHouse/server/platform/adapter/in/web/swagger/NoticeApiSpec.java similarity index 72% rename from src/main/java/com/myhome/server/platform/adapter/in/web/swagger/NoticeApiSpec.java rename to src/main/java/com/pinHouse/server/platform/adapter/in/web/swagger/NoticeApiSpec.java index 2dfd00a..31f8925 100644 --- a/src/main/java/com/myhome/server/platform/adapter/in/web/swagger/NoticeApiSpec.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/in/web/swagger/NoticeApiSpec.java @@ -1,9 +1,9 @@ -package com.myhome.server.platform.adapter.in.web.swagger; +package com.pinHouse.server.platform.adapter.in.web.swagger; -import com.myhome.server.core.response.response.ApiResponse; -import com.myhome.server.core.response.response.pageable.PageRequest; -import com.myhome.server.core.response.response.pageable.PageResponse; -import com.myhome.server.platform.adapter.in.web.dto.response.NoticeDTO; +import com.pinHouse.server.core.response.response.ApiResponse; +import com.pinHouse.server.core.response.response.pageable.PageRequest; +import com.pinHouse.server.core.response.response.pageable.PageResponse; +import com.pinHouse.server.platform.adapter.in.web.dto.response.NoticeDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/com/myhome/server/platform/adapter/out/NoticeMongoAdapter.java b/src/main/java/com/pinHouse/server/platform/adapter/out/NoticeMongoAdapter.java similarity index 71% rename from src/main/java/com/myhome/server/platform/adapter/out/NoticeMongoAdapter.java rename to src/main/java/com/pinHouse/server/platform/adapter/out/NoticeMongoAdapter.java index 89c3e53..c5fc44a 100644 --- a/src/main/java/com/myhome/server/platform/adapter/out/NoticeMongoAdapter.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/NoticeMongoAdapter.java @@ -1,9 +1,9 @@ -package com.myhome.server.platform.adapter.out; +package com.pinHouse.server.platform.adapter.out; -import com.myhome.server.platform.adapter.out.mongo.notice.NoticeDocument; -import com.myhome.server.platform.adapter.out.mongo.notice.NoticeDocumentRepository; -import com.myhome.server.platform.application.out.notice.NoticePort; -import com.myhome.server.platform.domain.notice.Notice; +import com.pinHouse.server.platform.adapter.out.mongo.notice.NoticeDocument; +import com.pinHouse.server.platform.adapter.out.mongo.notice.NoticeDocumentRepository; +import com.pinHouse.server.platform.application.out.notice.NoticePort; +import com.pinHouse.server.platform.domain.notice.Notice; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/pinHouse/server/platform/adapter/out/UserJpaAdapter.java b/src/main/java/com/pinHouse/server/platform/adapter/out/UserJpaAdapter.java new file mode 100644 index 0000000..29559a0 --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/UserJpaAdapter.java @@ -0,0 +1,50 @@ +package com.pinHouse.server.platform.adapter.out; + +import com.pinHouse.server.platform.adapter.out.jpa.user.UserJpaEntity; +import com.pinHouse.server.platform.adapter.out.jpa.user.UserJpaRepository; +import com.pinHouse.server.platform.application.out.user.UserPort; +import com.pinHouse.server.platform.domain.user.Provider; +import com.pinHouse.server.platform.domain.user.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class UserJpaAdapter implements UserPort { + + private final UserJpaRepository repository; + + @Override + public User saveUser(User user) { + var entity = UserJpaEntity.from(user); + + return repository.save(entity) + .toDomain(); + } + + @Override + public boolean checkExistingById(UUID userId) { + return repository.existsById(userId); + } + + @Override + public Optional loadUserById(UUID userId) { + return repository.findById(userId) + .map(UserJpaEntity::toDomain); + } + + @Override + public boolean existsByEmail(String email) { + Optional userJpaEntity = repository.findByEmail(email); + return userJpaEntity.isPresent(); + } + + @Override + public Optional loadUserBySocialAndSocialId(Provider social, String socialId) { + return repository.findByProviderAndSocialId(social, socialId) + .map(UserJpaEntity::toDomain); + } +} diff --git a/src/main/java/com/myhome/server/platform/adapter/out/external/distance/OdsayAdapter.java b/src/main/java/com/pinHouse/server/platform/adapter/out/external/distance/OdsayAdapter.java similarity index 91% rename from src/main/java/com/myhome/server/platform/adapter/out/external/distance/OdsayAdapter.java rename to src/main/java/com/pinHouse/server/platform/adapter/out/external/distance/OdsayAdapter.java index 5d83785..86e43b3 100644 --- a/src/main/java/com/myhome/server/platform/adapter/out/external/distance/OdsayAdapter.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/external/distance/OdsayAdapter.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.adapter.out.external.distance; +package com.pinHouse.server.platform.adapter.out.external.distance; -import com.myhome.server.platform.application.out.distance.DistancePort; +import com.pinHouse.server.platform.application.out.distance.DistancePort; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/BaseTimeEntity.java b/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/BaseTimeEntity.java new file mode 100644 index 0000000..e9bf836 --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/BaseTimeEntity.java @@ -0,0 +1,19 @@ +package com.pinHouse.server.platform.adapter.out.jpa; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + +} diff --git a/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/user/UserJpaEntity.java b/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/user/UserJpaEntity.java new file mode 100644 index 0000000..a823dfb --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/user/UserJpaEntity.java @@ -0,0 +1,87 @@ +package com.pinHouse.server.platform.adapter.out.jpa.user; + +import com.pinHouse.server.platform.adapter.out.jpa.BaseTimeEntity; +import com.pinHouse.server.platform.domain.user.Gender; +import com.pinHouse.server.platform.domain.user.Provider; +import com.pinHouse.server.platform.domain.user.Role; +import com.pinHouse.server.platform.domain.user.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; +import java.util.UUID; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "users") +@AllArgsConstructor +@Builder +public class UserJpaEntity extends BaseTimeEntity { + + @Id + @Column(name = "id", nullable = false, columnDefinition = "BINARY(16)") + private UUID id; + + @Enumerated(EnumType.STRING) + private Provider provider; + + private String socialId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, unique = true) + private String email; + + @Column(name = "phone_number") + private String phoneNumber; + + @Enumerated(EnumType.STRING) + private Role role; + + @Enumerated(EnumType.STRING) + private Gender gender; + + private String profileImage; + + private LocalDate birthday; + + @PrePersist + public void generateUUID() { + if (this.id == null) { + this.id = UUID.randomUUID(); + } + } + + public static UserJpaEntity from(User user) { + return UserJpaEntity.builder() + .id(user.getId()) + .name(user.getName()) + .provider(user.getProvider()) + .socialId(user.getSocialId()) + .email(user.getEmail()) + .phoneNumber(user.getPhoneNumber()) + .role(user.getRole()) + .gender(user.getGender()) + .profileImage(user.getProfileImage()) + .birthday(user.getBirthday()) + .build(); + } + + public User toDomain() { + return User.builder() + .id(id) + .provider(provider) + .socialId(socialId) + .name(name) + .email(email) + .phoneNumber(phoneNumber) + .role(role) + .gender(gender) + .profileImage(profileImage) + .birthday(birthday) + .build(); + } + +} diff --git a/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/user/UserJpaRepository.java b/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/user/UserJpaRepository.java new file mode 100644 index 0000000..0d082fc --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/jpa/user/UserJpaRepository.java @@ -0,0 +1,19 @@ +package com.pinHouse.server.platform.adapter.out.jpa.user; + +import com.pinHouse.server.platform.domain.user.Provider; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface UserJpaRepository extends JpaRepository { + Optional findByEmail(String email); + + Optional findById(UUID id); + + + Optional findByProviderAndSocialId(Provider social, String socialId); + +} diff --git a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/DepositDocument.java b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/DepositDocument.java similarity index 84% rename from src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/DepositDocument.java rename to src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/DepositDocument.java index a0326f1..bb55df0 100644 --- a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/DepositDocument.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/DepositDocument.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.adapter.out.mongo.notice; +package com.pinHouse.server.platform.adapter.out.mongo.notice; -import com.myhome.server.platform.domain.notice.NoticeSupplyInfo; +import com.pinHouse.server.platform.domain.notice.NoticeSupplyInfo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeDocument.java b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeDocument.java similarity index 94% rename from src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeDocument.java rename to src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeDocument.java index 0dec505..724ecfb 100644 --- a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeDocument.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeDocument.java @@ -1,7 +1,7 @@ -package com.myhome.server.platform.adapter.out.mongo.notice; +package com.pinHouse.server.platform.adapter.out.mongo.notice; -import com.myhome.server.platform.domain.location.Location; -import com.myhome.server.platform.domain.notice.Notice; +import com.pinHouse.server.platform.domain.location.Location; +import com.pinHouse.server.platform.domain.notice.Notice; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeDocumentRepository.java b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeDocumentRepository.java similarity index 80% rename from src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeDocumentRepository.java rename to src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeDocumentRepository.java index 3775d8d..aa06921 100644 --- a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeDocumentRepository.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeDocumentRepository.java @@ -1,4 +1,4 @@ -package com.myhome.server.platform.adapter.out.mongo.notice; +package com.pinHouse.server.platform.adapter.out.mongo.notice; import org.springframework.data.mongodb.repository.MongoRepository; diff --git a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeSupplyInfoDocument.java b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeSupplyInfoDocument.java similarity index 87% rename from src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeSupplyInfoDocument.java rename to src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeSupplyInfoDocument.java index e64c9b5..642e708 100644 --- a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/NoticeSupplyInfoDocument.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/NoticeSupplyInfoDocument.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.adapter.out.mongo.notice; +package com.pinHouse.server.platform.adapter.out.mongo.notice; -import com.myhome.server.platform.domain.notice.NoticeSupplyInfo; +import com.pinHouse.server.platform.domain.notice.NoticeSupplyInfo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/RecruitmentCountDocument.java b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/RecruitmentCountDocument.java similarity index 83% rename from src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/RecruitmentCountDocument.java rename to src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/RecruitmentCountDocument.java index b731587..da949b5 100644 --- a/src/main/java/com/myhome/server/platform/adapter/out/mongo/notice/RecruitmentCountDocument.java +++ b/src/main/java/com/pinHouse/server/platform/adapter/out/mongo/notice/RecruitmentCountDocument.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.adapter.out.mongo.notice; +package com.pinHouse.server.platform.adapter.out.mongo.notice; -import com.myhome.server.platform.domain.notice.NoticeSupplyInfo; +import com.pinHouse.server.platform.domain.notice.NoticeSupplyInfo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/myhome/server/platform/application/in/NoticeUseCase.java b/src/main/java/com/pinHouse/server/platform/application/in/NoticeUseCase.java similarity index 75% rename from src/main/java/com/myhome/server/platform/application/in/NoticeUseCase.java rename to src/main/java/com/pinHouse/server/platform/application/in/NoticeUseCase.java index 044648a..eb5f909 100644 --- a/src/main/java/com/myhome/server/platform/application/in/NoticeUseCase.java +++ b/src/main/java/com/pinHouse/server/platform/application/in/NoticeUseCase.java @@ -1,9 +1,9 @@ -package com.myhome.server.platform.application.in; +package com.pinHouse.server.platform.application.in; -import com.myhome.server.core.response.response.pageable.PageRequest; -import com.myhome.server.platform.adapter.in.web.dto.response.NoticeDTO; -import com.myhome.server.platform.adapter.in.web.dto.response.NoticeSupplyDTO; -import com.myhome.server.platform.domain.notice.Notice; +import com.pinHouse.server.core.response.response.pageable.PageRequest; +import com.pinHouse.server.platform.adapter.in.web.dto.response.NoticeDTO; +import com.pinHouse.server.platform.adapter.in.web.dto.response.NoticeSupplyDTO; +import com.pinHouse.server.platform.domain.notice.Notice; import org.springframework.data.domain.Page; import java.util.List; diff --git a/src/main/java/com/myhome/server/platform/application/out/distance/DistancePort.java b/src/main/java/com/pinHouse/server/platform/application/out/distance/DistancePort.java similarity index 84% rename from src/main/java/com/myhome/server/platform/application/out/distance/DistancePort.java rename to src/main/java/com/pinHouse/server/platform/application/out/distance/DistancePort.java index 4e9cc82..6b90cfd 100644 --- a/src/main/java/com/myhome/server/platform/application/out/distance/DistancePort.java +++ b/src/main/java/com/pinHouse/server/platform/application/out/distance/DistancePort.java @@ -1,4 +1,4 @@ -package com.myhome.server.platform.application.out.distance; +package com.pinHouse.server.platform.application.out.distance; import java.io.UnsupportedEncodingException; diff --git a/src/main/java/com/myhome/server/platform/application/out/notice/NoticePort.java b/src/main/java/com/pinHouse/server/platform/application/out/notice/NoticePort.java similarity index 76% rename from src/main/java/com/myhome/server/platform/application/out/notice/NoticePort.java rename to src/main/java/com/pinHouse/server/platform/application/out/notice/NoticePort.java index 43667d0..aa9b953 100644 --- a/src/main/java/com/myhome/server/platform/application/out/notice/NoticePort.java +++ b/src/main/java/com/pinHouse/server/platform/application/out/notice/NoticePort.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.application.out.notice; +package com.pinHouse.server.platform.application.out.notice; -import com.myhome.server.platform.domain.notice.Notice; +import com.pinHouse.server.platform.domain.notice.Notice; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/pinHouse/server/platform/application/out/user/UserPort.java b/src/main/java/com/pinHouse/server/platform/application/out/user/UserPort.java new file mode 100644 index 0000000..ac72137 --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/application/out/user/UserPort.java @@ -0,0 +1,20 @@ +package com.pinHouse.server.platform.application.out.user; + +import com.pinHouse.server.platform.domain.user.Provider; +import com.pinHouse.server.platform.domain.user.User; + +import java.util.Optional; +import java.util.UUID; + +public interface UserPort { + + Optional loadUserById(UUID Id); + + Optional loadUserBySocialAndSocialId(Provider socialType, String socialId); + + User saveUser(User user); + + boolean existsByEmail(String email); + + boolean checkExistingById(UUID userId); +} diff --git a/src/main/java/com/myhome/server/platform/application/service/NoticeService.java b/src/main/java/com/pinHouse/server/platform/application/service/NoticeService.java similarity index 91% rename from src/main/java/com/myhome/server/platform/application/service/NoticeService.java rename to src/main/java/com/pinHouse/server/platform/application/service/NoticeService.java index 2f16673..3f45dc0 100644 --- a/src/main/java/com/myhome/server/platform/application/service/NoticeService.java +++ b/src/main/java/com/pinHouse/server/platform/application/service/NoticeService.java @@ -1,13 +1,13 @@ -package com.myhome.server.platform.application.service; - -import com.myhome.server.core.response.response.ErrorCode; -import com.myhome.server.core.response.response.pageable.PageRequest; -import com.myhome.server.platform.adapter.in.web.dto.response.NoticeDTO; -import com.myhome.server.platform.adapter.in.web.dto.response.NoticeSupplyDTO; -import com.myhome.server.platform.application.in.NoticeUseCase; -import com.myhome.server.platform.application.out.notice.NoticePort; -import com.myhome.server.platform.domain.notice.Notice; -import com.myhome.server.platform.domain.notice.NoticeSupplyInfo; +package com.pinHouse.server.platform.application.service; + +import com.pinHouse.server.core.response.response.ErrorCode; +import com.pinHouse.server.core.response.response.pageable.PageRequest; +import com.pinHouse.server.platform.adapter.in.web.dto.response.NoticeDTO; +import com.pinHouse.server.platform.adapter.in.web.dto.response.NoticeSupplyDTO; +import com.pinHouse.server.platform.application.in.NoticeUseCase; +import com.pinHouse.server.platform.application.out.notice.NoticePort; +import com.pinHouse.server.platform.domain.notice.Notice; +import com.pinHouse.server.platform.domain.notice.NoticeSupplyInfo; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; diff --git a/src/main/java/com/myhome/server/platform/domain/BaseDomain.java b/src/main/java/com/pinHouse/server/platform/domain/BaseDomain.java similarity index 88% rename from src/main/java/com/myhome/server/platform/domain/BaseDomain.java rename to src/main/java/com/pinHouse/server/platform/domain/BaseDomain.java index cd12183..daa8c51 100644 --- a/src/main/java/com/myhome/server/platform/domain/BaseDomain.java +++ b/src/main/java/com/pinHouse/server/platform/domain/BaseDomain.java @@ -1,4 +1,4 @@ -package com.myhome.server.platform.domain; +package com.pinHouse.server.platform.domain; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/myhome/server/platform/domain/location/Location.java b/src/main/java/com/pinHouse/server/platform/domain/location/Location.java similarity index 90% rename from src/main/java/com/myhome/server/platform/domain/location/Location.java rename to src/main/java/com/pinHouse/server/platform/domain/location/Location.java index 48ca026..ec4125e 100644 --- a/src/main/java/com/myhome/server/platform/domain/location/Location.java +++ b/src/main/java/com/pinHouse/server/platform/domain/location/Location.java @@ -1,4 +1,4 @@ -package com.myhome.server.platform.domain.location; +package com.pinHouse.server.platform.domain.location; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/myhome/server/platform/domain/notice/Notice.java b/src/main/java/com/pinHouse/server/platform/domain/notice/Notice.java similarity index 87% rename from src/main/java/com/myhome/server/platform/domain/notice/Notice.java rename to src/main/java/com/pinHouse/server/platform/domain/notice/Notice.java index 512e9d2..fa4050d 100644 --- a/src/main/java/com/myhome/server/platform/domain/notice/Notice.java +++ b/src/main/java/com/pinHouse/server/platform/domain/notice/Notice.java @@ -1,6 +1,6 @@ -package com.myhome.server.platform.domain.notice; +package com.pinHouse.server.platform.domain.notice; -import com.myhome.server.platform.domain.location.Location; +import com.pinHouse.server.platform.domain.location.Location; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/myhome/server/platform/domain/notice/NoticeSupplyInfo.java b/src/main/java/com/pinHouse/server/platform/domain/notice/NoticeSupplyInfo.java similarity index 94% rename from src/main/java/com/myhome/server/platform/domain/notice/NoticeSupplyInfo.java rename to src/main/java/com/pinHouse/server/platform/domain/notice/NoticeSupplyInfo.java index 38994b5..f9902fb 100644 --- a/src/main/java/com/myhome/server/platform/domain/notice/NoticeSupplyInfo.java +++ b/src/main/java/com/pinHouse/server/platform/domain/notice/NoticeSupplyInfo.java @@ -1,4 +1,4 @@ -package com.myhome.server.platform.domain.notice; +package com.pinHouse.server.platform.domain.notice; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/pinHouse/server/platform/domain/notification/Notification.java b/src/main/java/com/pinHouse/server/platform/domain/notification/Notification.java new file mode 100644 index 0000000..8c6ec25 --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/domain/notification/Notification.java @@ -0,0 +1,6 @@ +package com.pinHouse.server.platform.domain.notification; + +import com.pinHouse.server.platform.domain.BaseDomain; + +public class Notification extends BaseDomain { +} diff --git a/src/main/java/com/pinHouse/server/platform/domain/user/Gender.java b/src/main/java/com/pinHouse/server/platform/domain/user/Gender.java new file mode 100644 index 0000000..ea9a1b8 --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/domain/user/Gender.java @@ -0,0 +1,34 @@ +package com.pinHouse.server.platform.domain.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum Gender { + + @JsonProperty("남성") + Male("M"), + + @JsonProperty("여성") + Female("F"), + + @JsonProperty("기타") + Other("U"); + + private final String value; + + /** + * value로 값 얻기 + * @param value 밸류 + */ + public static Gender getGender(String value) { + for (Gender gender : Gender.values()) { + if (gender.value.equals(value)) { + return gender; + } + } + throw new IllegalArgumentException("존재하지 않는 성별 코드입니다: " + value); + } + + +} diff --git a/src/main/java/com/pinHouse/server/platform/domain/user/Provider.java b/src/main/java/com/pinHouse/server/platform/domain/user/Provider.java new file mode 100644 index 0000000..4002a5a --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/domain/user/Provider.java @@ -0,0 +1,5 @@ +package com.pinHouse.server.platform.domain.user; + +public enum Provider { + KAKAO, NAVER +} diff --git a/src/main/java/com/pinHouse/server/platform/domain/user/Role.java b/src/main/java/com/pinHouse/server/platform/domain/user/Role.java new file mode 100644 index 0000000..4f8be8b --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/domain/user/Role.java @@ -0,0 +1,14 @@ +package com.pinHouse.server.platform.domain.user; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Role { + USER("ROLE_USER"), + ADMIN("ROLE_ADMIN"); + + private final String role; + +} diff --git a/src/main/java/com/pinHouse/server/platform/domain/user/User.java b/src/main/java/com/pinHouse/server/platform/domain/user/User.java new file mode 100644 index 0000000..cfc4092 --- /dev/null +++ b/src/main/java/com/pinHouse/server/platform/domain/user/User.java @@ -0,0 +1,70 @@ +package com.pinHouse.server.platform.domain.user; + +import com.pinHouse.server.core.util.BirthDayUtil; +import com.pinHouse.server.security.oauth2.domain.OAuth2UserInfo; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDate; +import java.util.UUID; + +/** + * 사용할 유저 도메인 입니다. + */ +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@SuperBuilder +public class User { + + private UUID id; + private Provider provider; + private String socialId; + private String name; + private String email; + private String phoneNumber; + private Role role; + private String profileImage; + private Gender gender; + private LocalDate birthday; + + /// 정적 팩토리 메서드 + public static User of(OAuth2UserInfo userInfo) { + return User.builder() + .id(UUID.randomUUID()) + .provider(Provider.valueOf(userInfo.getProvider())) + .socialId(userInfo.getProviderId()) + .name(userInfo.getUserName()) + .email(userInfo.getEmail()) + .phoneNumber("phoneNumber") + .profileImage(userInfo.getImageUrl()) + .birthday(BirthDayUtil.parseBirthday(userInfo.getBirthYear(), userInfo.getBirthday())) + .gender(Gender.getGender(userInfo.getGender())) + .role(Role.USER) + .build(); + } + + /// 정적 팩토리 메서드 + public static User of( + Provider provider, String socialId, String name, String email, + String profileImage, String phoneNumber, LocalDate birthday, Gender gender + ) { + return User.builder() + .id(UUID.randomUUID()) + .provider(provider) + .socialId(socialId) + .name(name) + .email(email) + .profileImage(profileImage) + .phoneNumber(phoneNumber) + .birthday(birthday) + .gender(gender) + .role(Role.USER) + .build(); + } + + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/domain/JwtToken.java b/src/main/java/com/pinHouse/server/security/jwt/domain/JwtToken.java new file mode 100644 index 0000000..c0a88de --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/domain/JwtToken.java @@ -0,0 +1,36 @@ +package com.pinHouse.server.security.jwt.domain; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.index.Indexed; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Getter +@Builder +@RedisHash +public class JwtToken { + + @Id + private UUID userId; + + @Indexed + private String refreshToken; + + @TimeToLive(unit = TimeUnit.DAYS) + private Long expireTime; + + /// 정적 팩토리 메소드 + public static JwtToken of(UUID userId, String refreshToken, Long expireTime) { + return JwtToken.builder() + .userId(userId) + .refreshToken(refreshToken) + .expireTime(expireTime) + .build(); + } + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/domain/JwtTokenRedisRepository.java b/src/main/java/com/pinHouse/server/security/jwt/domain/JwtTokenRedisRepository.java new file mode 100644 index 0000000..edac34d --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/domain/JwtTokenRedisRepository.java @@ -0,0 +1,17 @@ +package com.pinHouse.server.security.jwt.domain; + +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface JwtTokenRedisRepository extends CrudRepository { + + Optional findByRefreshToken(String refreshToken); + + void deleteByRefreshToken(String refreshToken); + + Optional findByUserId(UUID userId); + + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/exception/JwtAuthenticationException.java b/src/main/java/com/pinHouse/server/security/jwt/exception/JwtAuthenticationException.java new file mode 100644 index 0000000..980e9a7 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/exception/JwtAuthenticationException.java @@ -0,0 +1,10 @@ +package com.pinHouse.server.security.jwt.exception; + +import org.springframework.security.core.AuthenticationException; + +public class JwtAuthenticationException extends AuthenticationException { + + public JwtAuthenticationException(String s) { + super(s); + } +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationDeniedHandler.java b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationDeniedHandler.java new file mode 100644 index 0000000..9fa0e20 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationDeniedHandler.java @@ -0,0 +1,42 @@ +package com.pinHouse.server.security.jwt.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pinHouse.server.core.response.response.ApiResponse; +import com.pinHouse.server.core.response.response.CustomException; +import com.pinHouse.server.core.response.response.ErrorCode; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + + ErrorCode errorCode = ErrorCode.fromMessage(accessDeniedException.getMessage()); + + // 권한 부족 403 Error + CustomException exception = new CustomException(errorCode, null); + ApiResponse apiResponse = ApiResponse.fail(exception); + + /// response 제작 + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + // JSON 응답 + objectMapper.writeValue(response.getWriter(), apiResponse); + + } +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFailureHandler.java b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFailureHandler.java new file mode 100644 index 0000000..454b028 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFailureHandler.java @@ -0,0 +1,42 @@ +package com.pinHouse.server.security.jwt.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pinHouse.server.core.response.response.ApiResponse; +import com.pinHouse.server.core.response.response.CustomException; +import com.pinHouse.server.core.response.response.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFailureHandler implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + + // 로그인 필요 401 Error 발생 + ErrorCode errorCode = ErrorCode.fromMessage(authException.getMessage()); + + CustomException exception = new CustomException(errorCode, null); + ApiResponse apiResponse = ApiResponse.fail(exception); + + // 응답 설정 + response.setStatus(apiResponse.httpStatus().value()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + // JSON 응답 + objectMapper.writeValue(response.getWriter(), apiResponse); + } +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..cdba280 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFilter.java @@ -0,0 +1,119 @@ + package com.pinHouse.server.security.jwt.filter; + + import com.pinHouse.server.core.response.response.ErrorCode; + import com.pinHouse.server.security.jwt.exception.JwtAuthenticationException; + import com.pinHouse.server.security.jwt.util.JwtTokenExtractor; + import jakarta.servlet.FilterChain; + import jakarta.servlet.ServletException; + import jakarta.servlet.http.HttpServletRequest; + import jakarta.servlet.http.HttpServletResponse; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + import org.springframework.security.core.context.SecurityContextHolder; + import org.springframework.stereotype.Component; + import org.springframework.web.filter.OncePerRequestFilter; + + import java.io.IOException; + import java.util.Optional; + + import static com.pinHouse.server.core.response.response.ErrorCode.*; + + + @Slf4j + @Component + @RequiredArgsConstructor + public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenExtractor extractor; + private final JwtAuthenticationFailureHandler failureHandler; + private final RequestMatcherHolder requestMatcherHolder; + + public final static String JWT_ERROR = "jwtError"; + + // doFilterInternal + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + // OPTIONS 필터에서 타지않도록 넣는다. + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + filterChain.doFilter(request, response); + return; + } + + // + boolean isAnonymousAllowed = requestMatcherHolder.getRequestMatchersByMinRole(null) + .matches(request); + + // 토큰 추출 + try { + Optional token = extractor.extractAccessToken(request); + + if (isAnonymousAllowed) { + // anonymous 허용: 토큰 있으면 인증, 없으면 anonymous로 통과 + if (token.isPresent()) { + String accessToken = token.get(); + if (!extractor.validateToken(accessToken)) { + request.setAttribute(JWT_ERROR, INVALID_TOKEN); + throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN.getMessage()); + } + if (extractor.isExpired(accessToken)) { + request.setAttribute(JWT_ERROR, TOKEN_EXPIRED); + throw new JwtAuthenticationException(TOKEN_EXPIRED.getMessage()); + } + var authentication = extractor.getAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + return; + } + + // 토큰 검증 + // 비어있는 지 + if (token.isEmpty()) { + request.setAttribute(JWT_ERROR, TOKEN_NOT_FOUND); + throw new JwtAuthenticationException(TOKEN_NOT_FOUND.getMessage()); + } + + String accessToken = token.get(); + + // 타당한지 + if (!extractor.validateToken(accessToken)) { + request.setAttribute(JWT_ERROR, INVALID_TOKEN); + throw new JwtAuthenticationException(ErrorCode.INVALID_TOKEN.getMessage()); + } + + // 만료가 안되었는지 + if (extractor.isExpired(accessToken)) { + request.setAttribute(JWT_ERROR, TOKEN_EXPIRED); + throw new JwtAuthenticationException(TOKEN_EXPIRED.getMessage()); + } + + // 권한 생성하기 + var authentication = extractor.getAuthentication(token.get()); + + /// 시큐리티 홀더에 해당 멤버 저장 + SecurityContextHolder.getContext().setAuthentication(authentication); + + filterChain.doFilter(request, response); + + } catch (JwtAuthenticationException ex) { + + /// 실패 핸들러로 이동 + failureHandler.commence(request, response, ex); + } + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + + /// 상품 조회는 회원/비회원 구분해야되기에 모두 필터를 타도록 설정 + if (request.getRequestURI().startsWith("/api/v1/products")) { + return false; + } + + /// null 인 것 해결 + return requestMatcherHolder.getRequestMatchersByMinRole(null) + .matches(request); + } + + } diff --git a/src/main/java/com/pinHouse/server/security/jwt/filter/RequestMatcherHolder.java b/src/main/java/com/pinHouse/server/security/jwt/filter/RequestMatcherHolder.java new file mode 100644 index 0000000..e6c5b3d --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/filter/RequestMatcherHolder.java @@ -0,0 +1,93 @@ +package com.pinHouse.server.security.jwt.filter; + +import com.pinHouse.server.platform.domain.user.Role; +import io.micrometer.common.lang.Nullable; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import static org.springframework.http.HttpMethod.*; + +@Component +public class RequestMatcherHolder { + + private static final List REQUEST_INFO_LIST = List.of( + + // 공통 + new RequestInfo(OPTIONS, "/**", null), + new RequestInfo(GET, "/", null), + new RequestInfo(GET, "/login", null), + new RequestInfo(POST, "/api/v1/auth/dev-login", null), + + // auth + new RequestInfo(POST, "/api/v1/oauth2/**", null), + + // 유저 관련 + new RequestInfo(POST, "/api/v1/auth/reissue", null), + new RequestInfo(POST, "/api/v1/auth/logout", Role.USER), + + // 상품 관련 + new RequestInfo(GET, "/api/v1/products/**", null), + + // static resources + new RequestInfo(GET, "/docs/**", null), + new RequestInfo(GET, "/*.ico", null), + new RequestInfo(GET, "/resources/**", null), + new RequestInfo(GET, "/index.html", null), + new RequestInfo(GET, "/error", null), + new RequestInfo(GET, "/kikihi.png", null), + + // Swagger UI 및 API 문서 관련 요청 + new RequestInfo(GET, "/v3/api-docs/**", null), + new RequestInfo(GET, "/swagger-ui/**", null), + new RequestInfo(GET, "/swagger-resources/**", null), + new RequestInfo(GET, "/webjars/**", null), + new RequestInfo(GET, "/swagger-ui.html", null), + + // 정적 아이콘 요청 + new RequestInfo(GET, "/favicon.ico", null), + new RequestInfo(GET, "/apple-touch-icon.png", null), + + // 검색 (임시로 개방) + new RequestInfo(GET, "/api/v1/search", null) + + ); + + + + + private final ConcurrentHashMap reqMatcherCacheMap = new ConcurrentHashMap<>(); + + /** + * 최소 권한이 주어진 요청에 대한 RequestMatcher 반환 + * @param minRole 최소 권한 (Nullable) + * @return 생성된 RequestMatcher + */ + public RequestMatcher getRequestMatchersByMinRole(@Nullable Role minRole) { + var key = getKeyByRole(minRole); + return reqMatcherCacheMap.computeIfAbsent(key, k -> + new OrRequestMatcher(REQUEST_INFO_LIST.stream() + .filter(reqInfo -> isAccessible(reqInfo.minRole(), minRole)) + .map(reqInfo -> new AntPathRequestMatcher(reqInfo.pattern(), + reqInfo.method().name())) + .toArray(AntPathRequestMatcher[]::new))); + } + + private boolean isAccessible(@Nullable Role requiredRole, @Nullable Role currentRole) { + if (requiredRole == null) return true; // 누구나 접근 가능 + if (currentRole == null) return false; // 권한 없음 + return currentRole.ordinal() >= requiredRole.ordinal(); // ADMIN이면 MEMBER 포함 + } + + private String getKeyByRole(@Nullable Role minRole) { + return minRole == null ? "VISITOR" : minRole.name(); + } + + private record RequestInfo(HttpMethod method, String pattern, Role minRole) {} + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/service/JwtTokenService.java b/src/main/java/com/pinHouse/server/security/jwt/service/JwtTokenService.java new file mode 100644 index 0000000..dad08ff --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/service/JwtTokenService.java @@ -0,0 +1,115 @@ +package com.pinHouse.server.security.jwt.service; + +import com.pinHouse.server.core.response.response.ErrorCode; +import com.pinHouse.server.security.jwt.exception.JwtAuthenticationException; +import com.pinHouse.server.security.jwt.util.CookieUtil; +import com.pinHouse.server.security.jwt.util.JwtTokenExtractor; +import com.pinHouse.server.security.jwt.util.JwtTokenProvider; +import com.pinHouse.server.security.jwt.util.RedisUtil; +import com.pinHouse.server.security.oauth2.domain.PrincipalDetails; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Service +@Transactional +@RequiredArgsConstructor +public class JwtTokenService implements JwtTokenUseCase { + + private final JwtTokenProvider provider; + private final JwtTokenExtractor extractor; + + /// 레디스 + private final RedisUtil redisUtil; + + /// 쿠키 + private final CookieUtil cookieUtil; + + // 액세스 토큰 생성하기 + @Override + public void createAccessToken(HttpServletResponse response, Authentication authentication) { + // 토큰을 발급한다. + String accessToken = provider.generateAccessToken(authentication); + + // 토큰을 쿠키에 저장한다. + cookieUtil.setAccessCookie(accessToken, response); + + } + + // 리프레쉬 토큰 생성하기 + @Override + public void createRefreshToken(HttpServletResponse response, Authentication authentication) { + + // 토큰을 발급한다. + String refreshToken = provider.generateRefreshToken(authentication); + + // 인증 객체에서 정보 가져오기 + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + + // 해당 토큰을 레디스에 저장한다. + UUID userId = principalDetails.getId(); + redisUtil.saveRefreshToken(userId, refreshToken); + + // 토큰을 쿠키에 저장한다. + cookieUtil.setRefreshCookie(refreshToken, response); + } + + // 재발급 하기 + @Override + public void reissueByRefreshToken(HttpServletRequest request, HttpServletResponse response) { + + // 쿠키에서 리프레쉬 토큰 가져오기 + String refreshToken = cookieUtil.getRefreshTokenFromCookie(request) + .orElseThrow(() -> new JwtAuthenticationException(ErrorCode.TOKEN_NOT_FOUND_COOKIE.getMessage())); + + // 쿠키 검증 + if (!extractor.validateToken(refreshToken)) { + throw new JwtAuthenticationException(ErrorCode.TOKEN_INVALID.getMessage()); + }; + + // 인증 객체에서 정보 가져오기 + Authentication authentication = extractor.getAuthentication(refreshToken); + + // 유저 인증 조회 + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + + // 유저와 리프레쉬 토큰이 일치하는지 체크한다. + boolean checked = redisUtil.checkRefreshTokenAndUserId(refreshToken, principalDetails.getId()); + if (!checked) { + throw new JwtAuthenticationException(ErrorCode.INVALID_CREDENTIALS.getMessage()); + } + + // 쿠키에 다시 전송하기 + createAccessToken(response, authentication); + + } + + @Override + public void logout(UUID userId, HttpServletRequest request, HttpServletResponse response) { + + // 쿠키에서 리프레쉬 토큰 가져오기 + String refreshToken = cookieUtil.getRefreshTokenFromCookie(request) + .orElseThrow(() -> new JwtAuthenticationException(ErrorCode.TOKEN_NOT_FOUND_COOKIE.getMessage())); + + // 리프레쉬와 유저가 맞는지 체크 + boolean checked = redisUtil.checkRefreshTokenAndUserId(refreshToken, userId); + if (!checked) { + throw new JwtAuthenticationException(ErrorCode.INVALID_CREDENTIALS.getMessage()); + } + + // 리프레쉬 쿠키를 null 설정 + cookieUtil.deleteRefreshTokenCookie(response); + + // 액세스 쿠키를 null 설정 + cookieUtil.deleteAccessTokenCookie(response); + + // DB에서도 삭제 + redisUtil.deleteByRefreshToken(refreshToken); + } + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/service/JwtTokenUseCase.java b/src/main/java/com/pinHouse/server/security/jwt/service/JwtTokenUseCase.java new file mode 100644 index 0000000..ce8cf78 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/service/JwtTokenUseCase.java @@ -0,0 +1,23 @@ +package com.pinHouse.server.security.jwt.service; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; + +import java.util.UUID; + +public interface JwtTokenUseCase { + + // 액세스 토큰 생성하기 + void createAccessToken(HttpServletResponse response, Authentication authentication); + + // 리프레쉬 토큰 생성하기 + void createRefreshToken(HttpServletResponse response, Authentication authentication); + + // 재발급 하기 + void reissueByRefreshToken(HttpServletRequest request, HttpServletResponse response); + + // 로그아웃 진행하기 + void logout(UUID userId, HttpServletRequest request, HttpServletResponse response); + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/util/CookieUtil.java b/src/main/java/com/pinHouse/server/security/jwt/util/CookieUtil.java new file mode 100644 index 0000000..1c5fc26 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/util/CookieUtil.java @@ -0,0 +1,108 @@ +package com.pinHouse.server.security.jwt.util; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.pinHouse.server.security.jwt.util.TokenNameUtil.ACCESS_TOKEN_COOKIE_NAME; +import static com.pinHouse.server.security.jwt.util.TokenNameUtil.REFRESH_TOKEN_COOKIE_NAME; + + +@Slf4j +@Component +public class CookieUtil { + + @Value("${kikihi.jwt.access.expiration}") + private Long accessTokenExpiration; + + @Value("${kikihi.jwt.refresh.expiration}") + private Long refreshTokenExpiration; + + @Value("${kikihi.auth.jwt.secureOption}") + private boolean secureOption; + + @Value("${kikihi.auth.jwt.sameSiteOption}") + private String sameSiteOption; + + @Value("${kikihi.auth.jwt.cookiePathOption}") + private String cookiePathOption; + + // 쿠키 저장 + public void setAccessCookie(String accessToken, HttpServletResponse response) { + setCookie(response, ACCESS_TOKEN_COOKIE_NAME, accessToken, accessTokenExpiration); + } + + public void setRefreshCookie(String refreshToken, HttpServletResponse response) { + setCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, refreshTokenExpiration); + } + + // 쿠키 조회 + public Optional getAccessTokenFromCookie(HttpServletRequest request) { + return getTokenFromCookie(request, ACCESS_TOKEN_COOKIE_NAME); + } + + public Optional getRefreshTokenFromCookie(HttpServletRequest request) { + return getTokenFromCookie(request, REFRESH_TOKEN_COOKIE_NAME); + } + + // 쿠키 삭제 + public void deleteAccessTokenCookie(HttpServletResponse response) { + deleteCookie(response, ACCESS_TOKEN_COOKIE_NAME); + } + + public void deleteRefreshTokenCookie(HttpServletResponse response) { + deleteCookie(response, REFRESH_TOKEN_COOKIE_NAME); + } + + + + // 공통 쿠키 저장 메서드 + private void setCookie(HttpServletResponse response, String cookieName, String tokenValue, long maxAge) { + ResponseCookie cookie = ResponseCookie.from(cookieName, tokenValue) + .maxAge(maxAge) + .path(cookiePathOption) + .httpOnly(true) + .secure(secureOption) // Dev/Prod 환경에 따라 설정됨 + .sameSite(sameSiteOption) + .build(); + + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + } + + + // 공통 쿠키 추출 메서드 + private Optional getTokenFromCookie(HttpServletRequest request, String cookieName) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookieName.equals(cookie.getName())) { + return Optional.ofNullable(cookie.getValue()); + } + } + } + return Optional.empty(); + } + + + + // 공통 쿠키 삭제 메서드 + private void deleteCookie(HttpServletResponse response, String cookieName) { + ResponseCookie cookie = ResponseCookie.from(cookieName, "") + .maxAge(0) + .path(cookiePathOption) + .secure(secureOption) + .httpOnly(true) + .sameSite(sameSiteOption) + .build(); + + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + } + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/util/JwtTokenExtractor.java b/src/main/java/com/pinHouse/server/security/jwt/util/JwtTokenExtractor.java new file mode 100644 index 0000000..c910d8c --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/util/JwtTokenExtractor.java @@ -0,0 +1,148 @@ +package com.pinHouse.server.security.jwt.util; + +import com.pinHouse.server.core.response.response.ErrorCode; +import com.pinHouse.server.platform.application.out.user.UserPort; +import com.pinHouse.server.platform.domain.user.User; +import com.pinHouse.server.security.jwt.exception.JwtAuthenticationException; +import com.pinHouse.server.security.oauth2.domain.PrincipalDetails; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.*; + +import static com.pinHouse.server.security.jwt.util.TokenNameUtil.*; +import static org.springframework.security.oauth2.core.OAuth2ErrorCodes.INVALID_TOKEN; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtTokenExtractor { + + @Value("${kikihi.jwt.key}") + private String key; + + private SecretKey secretKey; + + /// 의존성 + private final UserPort userPort; + private final CookieUtil cookieUtil; + + + /// JWT 의존성은 SecretKey 필요 + @PostConstruct + private void setSecretKey() { + secretKey = Keys.hmacShaKeyFor(key.getBytes()); + } + + /// 토큰 추출하기 + public Optional extractAccessToken(HttpServletRequest request) { + return cookieUtil.getAccessTokenFromCookie(request); + } + + public Optional extractRefreshToken(HttpServletRequest request) { + return cookieUtil.getRefreshTokenFromCookie(request); + } + + /// 내부 함수 + private Claims parseClaims(String token) { + try { + // JWT 파서를 빌드하고 서명된 토큰을 파싱 + return Jwts.parserBuilder() + .setSigningKey(secretKey) // 서명 키를 설정 + .build() + .parseClaimsJws(token) // 서명된 JWT 토큰을 파싱 + .getBody(); // Claims 객체 반환 + } catch (ExpiredJwtException e) { + return e.getClaims(); + + } catch (MalformedJwtException e) { + throw new JwtAuthenticationException(INVALID_TOKEN); + } + } + + /// 검증 여부 + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token); + + return true; + } catch (ExpiredJwtException e) { + throw new JwtAuthenticationException(ErrorCode.TOKEN_EXPIRED.getMessage()); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtAuthenticationException(ErrorCode.TOKEN_NOT_FOUND.getMessage()); // 여기가 문제라면 메시지 바꿔도 좋음 + } + } + + public Authentication getAuthentication(String token) { + + /// JWT 파싱 + Claims claims = parseClaims(token); + + /// 권한 정보 가져오기 + List authoritiesList = (List) claims.get(ROLE_CLAIM); + + // 권한을 SimpleGrantedAuthority로 변환 + Collection authorities = + authoritiesList.stream() + .map(SimpleGrantedAuthority::new) + .toList(); + + // userId를 String 변환 + String claimUserId = claims.get(ID_CLAIM, String.class); + + /// UUID 변환 + UUID userId = UUID.fromString(claimUserId); + + // 해당 userId로 Member를 조회 + User user = userPort.loadUserById(userId) + .orElseThrow(() -> new JwtAuthenticationException(ErrorCode.USER_NOT_FOUND_IN_COOKIE.getMessage())); + + PrincipalDetails details = PrincipalDetails.of(user); + + /// SecurityContext에 저장하기 위한 UsernamePasswordAuthenticationToken 반환 + return new UsernamePasswordAuthenticationToken(details, token, authorities); + } + + /// @Getter + // 사용자 정보 추출 + public String getId(String token) { + return getIdFromToken(token, ID_CLAIM); + } + + public String getEmail(String token) { + return getClaimFromToken(token, EMAIL_CLAIM); + } + + public String getRole(String token) { + return getClaimFromToken(token, ROLE_CLAIM); + } + + public Boolean isExpired(String token) { + Claims claims = parseClaims(token); + return claims.getExpiration().before(new Date()); + } + + private String getClaimFromToken(String token, String claimName) { + Claims claims = parseClaims(token); + return claims.get(claimName, String.class); + } + + private String getIdFromToken(String token, String claimName) { + Claims claims = parseClaims(token); + return claims.get(claimName, String.class); + } +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/util/JwtTokenProvider.java b/src/main/java/com/pinHouse/server/security/jwt/util/JwtTokenProvider.java new file mode 100644 index 0000000..8639f58 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/util/JwtTokenProvider.java @@ -0,0 +1,84 @@ +package com.pinHouse.server.security.jwt.util; + +import com.pinHouse.server.security.oauth2.domain.PrincipalDetails; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.*; + +import static com.pinHouse.server.security.jwt.util.TokenNameUtil.ID_CLAIM; +import static com.pinHouse.server.security.jwt.util.TokenNameUtil.ROLE_CLAIM; + + +@Component +public class JwtTokenProvider { + + @Value("${kikihi.jwt.key}") + private String key; + + @Value("${kikihi.jwt.access.expiration}") + private Long accessTokenExpiration; + + @Value("${kikihi.jwt.refresh.expiration}") + private Long refreshTokenExpiration; + + private SecretKey secretKey; + + + @PostConstruct + private void setSecretKey() { + secretKey = Keys.hmacShaKeyFor(key.getBytes()); + } + + // Access token 발급 + public String generateAccessToken(Authentication authentication) { + return generateToken(authentication, accessTokenExpiration); + } + + // Refresh token 발급 + public String generateRefreshToken(Authentication authentication) { + return generateToken(authentication, refreshTokenExpiration); + } + + /// 토큰 생성 함수 + public String generateToken(Authentication authentication, long expireTime) { + + /// 시간 설정 + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + expireTime); + + /// 인증에서 객체 가져오기 + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + + /// 권한 리스트 추출 + Collection collection = principalDetails.getAuthorities(); + + // String 형태로 변환 + List authorities = collection.stream() + .map(GrantedAuthority::getAuthority) + .toList(); + + /// JWT 내용 생성 + Map claims = new HashMap<>(); + claims.put(ID_CLAIM, principalDetails.getId()); + claims.put(ROLE_CLAIM, authorities); + + // 토큰 반환 + return Jwts.builder() + .setSubject(String.valueOf(principalDetails.getId())) // 사용자 Id + .setClaims(claims) + .setIssuedAt(now) // 발급 시간 + .setExpiration(expiredDate) // 만료 시간 + .signWith(secretKey, SignatureAlgorithm.HS512) // 서명 + .compact(); + } + + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/util/RedisUtil.java b/src/main/java/com/pinHouse/server/security/jwt/util/RedisUtil.java new file mode 100644 index 0000000..72483d4 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/util/RedisUtil.java @@ -0,0 +1,71 @@ +package com.pinHouse.server.security.jwt.util; + +import com.pinHouse.server.core.response.response.ErrorCode; +import com.pinHouse.server.security.jwt.domain.JwtToken; +import com.pinHouse.server.security.jwt.domain.JwtTokenRedisRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.NoSuchElementException; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class RedisUtil { + + @Value("${kikihi.jwt.refresh.expiration}") + private Long refreshTokenExpiration; + + private final JwtTokenRedisRepository repository; + + + /// 리프레쉬 토큰 저장 + public void saveRefreshToken(UUID userId, String refreshToken) { + + // TTL 설정 + LocalDateTime plusSeconds = LocalDateTime.now().plusSeconds(refreshTokenExpiration); + Long expiredAt = plusSeconds.atZone(ZoneId.systemDefault()).toEpochSecond(); + + // 해당 토큰을 레디스에 저장한다. + JwtToken jwtToken = JwtToken.of(userId, refreshToken, expiredAt); + repository.save(jwtToken); + + } + + + /// 리프레쉬 토큰 조회 + public String getRefreshTokenByUserId(UUID userId) { + + /// 유저 Id로 리프레쉬 토큰 조회 + JwtToken jwtToken = repository.findByUserId(userId) + .orElseThrow(() -> new NoSuchElementException(ErrorCode.REFRESH_TOKEN_NOT_FOUND.getMessage())); + + return jwtToken.getRefreshToken(); + } + + public String getRefreshTokenById(String refreshToken) { + + /// 리프레쉬 토큰으로 유저 조회 + JwtToken jwtToken = repository.findByRefreshToken(refreshToken) + .orElseThrow(() -> new NoSuchElementException(ErrorCode.REFRESH_TOKEN_NOT_FOUND.getMessage())); + + return jwtToken.getRefreshToken(); + } + + /// 존재 여부 체크 + public boolean checkRefreshTokenAndUserId(String refreshToken, UUID userId) { + return repository.findByRefreshToken(refreshToken) + .map(token -> token.getUserId().equals(userId)) + .orElse(false); + } + + /// 리프레쉬 토큰 삭제하기 + public void deleteByRefreshToken(String refreshToken) { + + repository.deleteByRefreshToken(refreshToken); + } + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/util/TokenNameUtil.java b/src/main/java/com/pinHouse/server/security/jwt/util/TokenNameUtil.java new file mode 100644 index 0000000..66b0fe8 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/jwt/util/TokenNameUtil.java @@ -0,0 +1,25 @@ +package com.pinHouse.server.security.jwt.util; + +import java.util.UUID; + +public class TokenNameUtil { + + // 쿠키 이름 + public static final String ACCESS_TOKEN_COOKIE_NAME = "ACCESS_TOKEN"; + public static final String REFRESH_TOKEN_COOKIE_NAME = "REFRESH_TOKEN"; + + /// + public static final String ACCESS_TOKEN_SUBJECT = "Authorization"; + public static final String REFRESH_TOKEN_SUBJECT = "RefreshToken"; + + /// JWT + public static final String ID_CLAIM = "user_id"; + public static final String EMAIL_CLAIM = "email"; + public static final String ROLE_CLAIM = "role"; + public static final String ROLE_PREFIX = "ROLE_"; + + /// 레디스 키 + public static String getTokenKey(UUID userId) { + return "refreshToken:" + userId; + } +} diff --git a/src/main/java/com/pinHouse/server/security/oauth2/domain/OAuth2UserInfo.java b/src/main/java/com/pinHouse/server/security/oauth2/domain/OAuth2UserInfo.java new file mode 100644 index 0000000..bc01f93 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/oauth2/domain/OAuth2UserInfo.java @@ -0,0 +1,13 @@ +package com.pinHouse.server.security.oauth2.domain; + +public interface OAuth2UserInfo { + + String getProvider(); + String getProviderId(); + String getEmail(); + String getUserName(); + String getImageUrl(); + String getGender(); + String getBirthday(); + String getBirthYear(); +} diff --git a/src/main/java/com/pinHouse/server/security/oauth2/domain/PrincipalDetails.java b/src/main/java/com/pinHouse/server/security/oauth2/domain/PrincipalDetails.java new file mode 100644 index 0000000..66e8f54 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/oauth2/domain/PrincipalDetails.java @@ -0,0 +1,63 @@ +package com.pinHouse.server.security.oauth2.domain; + +import com.pinHouse.server.platform.domain.user.User; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +@Getter +@Builder +@RequiredArgsConstructor +public class PrincipalDetails implements OAuth2User{ + + private final User user; + private final Map attributes; + + // 생성자 + public static PrincipalDetails of(User user, Map attributes) { + return PrincipalDetails.builder() + .user(user) + .attributes(attributes) + .build(); + } + + public static PrincipalDetails of(User user) { + return PrincipalDetails.builder() + .user(user) + .attributes(null) + .build(); + } + + /// 로직 + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return Collections.singleton(new SimpleGrantedAuthority(this.user.getRole().getRole())); + } + + @Override + public String getName() { + return user.getName(); + } + + public UUID getId() { + return user.getId(); + } + + public String getEmail() { + return user.getEmail(); + } + +} diff --git a/src/main/java/com/pinHouse/server/security/oauth2/domain/kakao/KakaoUserInfo.java b/src/main/java/com/pinHouse/server/security/oauth2/domain/kakao/KakaoUserInfo.java new file mode 100644 index 0000000..2a05941 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/oauth2/domain/kakao/KakaoUserInfo.java @@ -0,0 +1,79 @@ +package com.pinHouse.server.security.oauth2.domain.kakao; + +import com.pinHouse.server.security.oauth2.domain.OAuth2UserInfo; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +@RequiredArgsConstructor +public class KakaoUserInfo implements OAuth2UserInfo { + + private final Map attributes; + private final Map account; + private final Map profile; + + @SuppressWarnings("unchecked") + public KakaoUserInfo(Map attributes) { + this.attributes = attributes; + this.account = Optional.ofNullable(attributes.get("kakao_account")) + .filter(Map.class::isInstance) + .map(m -> (Map) m) + .orElse(Collections.emptyMap()); + + this.profile = Optional.ofNullable(account.get("profile")) + .filter(Map.class::isInstance) + .map(m -> (Map) m) + .orElse(Collections.emptyMap()); + } + + @Override + public String getProvider() { + return "KAKAO"; + } + + @Override + public String getProviderId() { + return Optional.ofNullable(attributes.get("id")) + .map(Object::toString) + .orElse("Unknown"); + } + + @Override + public String getEmail() { + return Optional.ofNullable(account.get("email")) + .map(Object::toString) + .orElse("No email provided"); + } + + @Override + public String getUserName() { + return Optional.ofNullable(profile.get("nickname")) + .map(Object::toString) + .orElse("No name provided"); + } + + @Override + public String getImageUrl() { + + return Optional.ofNullable(profile.get("profile_image_url")) + .map(Object::toString) + .orElse("No image provided"); + } + + @Override + public String getGender() { + return ""; + } + + @Override + public String getBirthday() { + return ""; + } + + @Override + public String getBirthYear() { + return ""; + } +} diff --git a/src/main/java/com/pinHouse/server/security/oauth2/domain/naver/NaverUserInfo.java b/src/main/java/com/pinHouse/server/security/oauth2/domain/naver/NaverUserInfo.java new file mode 100644 index 0000000..eb60cc0 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/oauth2/domain/naver/NaverUserInfo.java @@ -0,0 +1,72 @@ +package com.pinHouse.server.security.oauth2.domain.naver; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.pinHouse.server.security.oauth2.domain.OAuth2UserInfo; + +import java.util.Map; +import java.util.Optional; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class NaverUserInfo implements OAuth2UserInfo { + + private final Map response; + + public NaverUserInfo(Map attributes) { + this.response = (Map) attributes.get("response"); + } + + @Override + public String getProviderId() { + return Optional.ofNullable(response.get("id")) + .map(Object::toString) + .orElse("Unknown"); + } + + @Override + public String getProvider() { + return "NAVER"; + } + + @Override + public String getEmail() { + return Optional.ofNullable(response.get("email")) + .map(Object::toString) + .orElse("No email provided"); + } + + @Override + public String getUserName() { + return Optional.ofNullable(response.get("name")) + .map(Object::toString) + .orElse("No name provided"); + } + + @Override + public String getImageUrl() { + return Optional.ofNullable(response.get("profile_image")) + .map(Object::toString) + .orElse("No image provided"); + } + + @Override + public String getGender() { + return Optional.ofNullable(response.get("gender")) + .map(Object::toString) + .orElse("No gender provided"); + } + + @Override + public String getBirthday() { + return Optional.ofNullable(response.get("birthday")) + .map(Object::toString) + .orElse("No birthday provided"); + } + + @Override + public String getBirthYear() { + return Optional.ofNullable(response.get("birthyear")) + .map(Object::toString) + .orElse("No birthyear provided"); + } + +} diff --git a/src/main/java/com/pinHouse/server/security/oauth2/handler/OAuth2SuccessHandler.java b/src/main/java/com/pinHouse/server/security/oauth2/handler/OAuth2SuccessHandler.java new file mode 100644 index 0000000..adbb8db --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/oauth2/handler/OAuth2SuccessHandler.java @@ -0,0 +1,47 @@ +package com.pinHouse.server.security.oauth2.handler; + +import com.pinHouse.server.security.jwt.service.JwtTokenUseCase; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenUseCase tokenService; + + @Value("${spring.front.host}") + public String REDIRECT_PATH; + + /* + 기존에 존재하는 유저의 경우, 토큰 발급을 진행합니다. + 리다이렉트 시킵니다. + */ + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + + /// AccessToken과 Refresh 토큰 생성 + + tokenService.createAccessToken(response, authentication); + tokenService.createRefreshToken(response, authentication); + + /// 시큐리티 홀더에 해당 멤버 저장 + SecurityContextHolder.getContext().setAuthentication(authentication); + + log.info("시큐리티에 저장된 인증 정보 :{}", authentication.getPrincipal().toString()); + + // 쿠키와 함께 리다이렉트 (프론트 홈 주소) + getRedirectStrategy().sendRedirect(request, response, REDIRECT_PATH); + } +} diff --git a/src/main/java/com/pinHouse/server/security/oauth2/service/OAuth2UserService.java b/src/main/java/com/pinHouse/server/security/oauth2/service/OAuth2UserService.java new file mode 100644 index 0000000..12ca9e7 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/oauth2/service/OAuth2UserService.java @@ -0,0 +1,114 @@ +package com.pinHouse.server.security.oauth2.service; + +import com.pinHouse.server.platform.application.out.user.UserPort; +import com.pinHouse.server.platform.domain.user.Gender; +import com.pinHouse.server.platform.domain.user.Provider; +import com.pinHouse.server.platform.domain.user.User; +import com.pinHouse.server.security.oauth2.domain.OAuth2UserInfo; +import com.pinHouse.server.security.oauth2.domain.PrincipalDetails; +import com.pinHouse.server.security.oauth2.domain.kakao.KakaoUserInfo; +import com.pinHouse.server.security.oauth2.domain.naver.NaverUserInfo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.Optional; + +import static com.pinHouse.server.core.util.BirthDayUtil.parseBirthday; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class OAuth2UserService extends DefaultOAuth2UserService { + + private final UserPort userPort; + + /** + * 소셜 로그인 유저 가져오기 + * @param userRequest 정보 요청 + * @return 유저 + */ + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + /// 유저 정보(attributes) 가져오기 + Map oAuth2UserAttributes = super.loadUser(userRequest).getAttributes(); + + /// resistrationId 가져오기 + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + + /// userNameAttributeName 가져오기 + String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails() + .getUserInfoEndpoint().getUserNameAttributeName(); + + log.info(registrationId + ": " + userNameAttributeName); + + /// OAuth2를 바탕으로 정보 생성 + OAuth2UserInfo userInfo = null; + + /// Provider 에 따른 유저 생성 + userInfo = createOAuth2User(registrationId, oAuth2UserAttributes); + + Provider social = Provider.valueOf(userInfo.getProvider()); + Optional existUser = userPort.loadUserBySocialAndSocialId(social, userInfo.getProviderId()); + + /// 존재한다면 로그인 + if (existUser.isPresent()) { + /// 이미 존재하는 유저를 반환한다. + + User user = existUser.get(); + log.info("이미 로그인한 유저입니다."); + + return PrincipalDetails.of(user, oAuth2UserAttributes); + + } else { + /// 정보를 통해 임시 저장한 뒤, 개인정보 추가하도록 한다. + + User user = User.of( + Provider.valueOf(userInfo.getProvider()), + userInfo.getProviderId(), + userInfo.getUserName(), + userInfo.getEmail(), + userInfo.getImageUrl(), + null, + parseBirthday(userInfo.getBirthYear(), userInfo.getBirthday()), + Gender.getGender(userInfo.getGender()) + ); + + User savedNewUser = userPort.saveUser(user); + + return PrincipalDetails.of(savedNewUser, oAuth2UserAttributes); + } + } + + + /** + * 소셜로그인 유저 생성 + * @param registrationId 소셜로그인 종류 + * @param oAuth2UserAttributes 소셜로그인 정보 + */ + private OAuth2UserInfo createOAuth2User(String registrationId, Map oAuth2UserAttributes) { + OAuth2UserInfo userInfo; + + switch (registrationId.toUpperCase()) { + case "KAKAO": + userInfo = new KakaoUserInfo(oAuth2UserAttributes); + break; + case "NAVER": + userInfo = new NaverUserInfo(oAuth2UserAttributes); + break; + default: + throw new IllegalArgumentException("Unknown provider: " + registrationId); + } + + return userInfo; + } + +} diff --git a/src/test/java/com/myhome/server/ServerApplicationTests.java b/src/test/java/com/pinHouse/server/ServerApplicationTests.java similarity index 63% rename from src/test/java/com/myhome/server/ServerApplicationTests.java rename to src/test/java/com/pinHouse/server/ServerApplicationTests.java index c96aeee..e12d1e3 100644 --- a/src/test/java/com/myhome/server/ServerApplicationTests.java +++ b/src/test/java/com/pinHouse/server/ServerApplicationTests.java @@ -1,9 +1,11 @@ -package com.myhome.server; +package com.pinHouse.server; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class ServerApplicationTests { @Test