diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 2675453c..51088305 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,8 +1,8 @@ --- name: Report bug about: 오류가 발생한 영역에 대해 보고합니다 [Dev] -title: "[BUG] " -labels: bug +title: "[오류] " +labels: 🐞 BugFix assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index c5f818a0..74c97725 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,8 +1,8 @@ --- name: Feature request about: Github Discussion, 회의에서 화제가 된 주제에 대해 제안합니다 [Moderator, Chef] -title: '[REQ]' -labels: enhancement +title: "[기능] " +labels: ✨ Feature assignees: '' --- diff --git a/.github/pr-template.md b/.github/pull_request_template.md similarity index 99% rename from .github/pr-template.md rename to .github/pull_request_template.md index 2d54b96a..c920935a 100644 --- a/.github/pr-template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,12 @@ ## 💡 다음 이슈를 해결했어요. +### Issue Link - #1 + - (가능한 한 자세히 작성해 주시면 도움이 됩니다.)
## 💡 이슈를 처리하면서 추가된 코드가 있어요. -### Issue Link - #1 - (없다면 이 문항을 지워주세요.) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..cc8cb265 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +name: Build-Test + +on: + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + - name: Build with Gradle + run: ./gradlew build diff --git a/src/main/java/camp/woowak/lab/common/advice/DomainExceptionHandler.java b/src/main/java/camp/woowak/lab/common/advice/DomainExceptionHandler.java new file mode 100644 index 00000000..56a9c564 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/advice/DomainExceptionHandler.java @@ -0,0 +1,32 @@ +package camp.woowak.lab.common.advice; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RestControllerAdvice +@Order(Ordered.LOWEST_PRECEDENCE - 1) +public @interface DomainExceptionHandler { + @AliasFor(annotation = RestControllerAdvice.class, attribute = "basePackages") + String[] basePackages() default {}; + + @AliasFor(annotation = RestControllerAdvice.class, attribute = "basePackageClasses") + Class[] basePackageClasses() default {}; + + @AliasFor(annotation = RestControllerAdvice.class, attribute = "assignableTypes") + Class[] assignableTypes() default {}; + + @AliasFor(annotation = RestControllerAdvice.class, attribute = "annotations") + Class[] annotations() default {}; +} diff --git a/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java b/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java new file mode 100644 index 00000000..69c86318 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/advice/GlobalExceptionHandler.java @@ -0,0 +1,24 @@ +package camp.woowak.lab.common.advice; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAllUncaughtException(Exception e) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, + e.getMessage()); + problemDetail.setProperty("errorCode", "9999"); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(problemDetail); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java b/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java new file mode 100644 index 00000000..3b4d4090 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class BadRequestException extends HttpStatusException { + public BadRequestException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/ConflictException.java b/src/main/java/camp/woowak/lab/common/exception/ConflictException.java new file mode 100644 index 00000000..e7b16219 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/ConflictException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class ConflictException extends HttpStatusException { + public ConflictException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/ErrorCode.java b/src/main/java/camp/woowak/lab/common/exception/ErrorCode.java new file mode 100644 index 00000000..54a2beda --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/ErrorCode.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public interface ErrorCode { + int getStatus(); + String getErrorCode(); + String getMessage(); +} diff --git a/src/main/java/camp/woowak/lab/common/exception/ForbiddenException.java b/src/main/java/camp/woowak/lab/common/exception/ForbiddenException.java new file mode 100644 index 00000000..05ab0863 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/ForbiddenException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class ForbiddenException extends HttpStatusException { + public ForbiddenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/HttpStatusException.java b/src/main/java/camp/woowak/lab/common/exception/HttpStatusException.java new file mode 100644 index 00000000..b9c07ac1 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/HttpStatusException.java @@ -0,0 +1,14 @@ +package camp.woowak.lab.common.exception; + +public class HttpStatusException extends RuntimeException { + private final ErrorCode errorCode; + + public HttpStatusException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode errorCode() { + return errorCode; + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/MethodNotAllowedException.java b/src/main/java/camp/woowak/lab/common/exception/MethodNotAllowedException.java new file mode 100644 index 00000000..6704a8cd --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/MethodNotAllowedException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class MethodNotAllowedException extends HttpStatusException { + public MethodNotAllowedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/NotFoundException.java b/src/main/java/camp/woowak/lab/common/exception/NotFoundException.java new file mode 100644 index 00000000..6928975e --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class NotFoundException extends HttpStatusException { + public NotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java b/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java new file mode 100644 index 00000000..ba2e1237 --- /dev/null +++ b/src/main/java/camp/woowak/lab/common/exception/UnauthorizedException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.common.exception; + +public class UnauthorizedException extends HttpStatusException { + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/camp/woowak/lab/web/api/utils/APIResponse.java b/src/main/java/camp/woowak/lab/web/api/utils/APIResponse.java new file mode 100644 index 00000000..e41f44a8 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/utils/APIResponse.java @@ -0,0 +1,25 @@ +package camp.woowak.lab.web.api.utils; + +import org.springframework.http.HttpStatus; + +/** + * APIResponse를 Jackson의 ObjectMapper와 함께 사용하려면, + * Generic Type의 {@code data}에는 Getter 메서드가 필요합니다. + */ +public class APIResponse { + private final T data; + private final int status; + + APIResponse(final HttpStatus status, final T data) { + this.data = data; + this.status = status.value(); + } + + public T getData() { + return data; + } + + public int getStatus() { + return status; + } +} diff --git a/src/main/java/camp/woowak/lab/web/api/utils/APIUtils.java b/src/main/java/camp/woowak/lab/web/api/utils/APIUtils.java new file mode 100644 index 00000000..9bdb3c68 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/utils/APIUtils.java @@ -0,0 +1,19 @@ +package camp.woowak.lab.web.api.utils; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +/** + * + * API Utils + * of Method는 status와 data를 이용해 APIResponse객체를 만들 수 있습니다. + * + */ +public final class APIUtils { + private APIUtils() { + } + + public static ResponseEntity> of(HttpStatus status, T data) { + return new ResponseEntity<>(new APIResponse<>(status, data), status); + } +} diff --git a/src/test/java/camp/woowak/lab/web/api/utils/APIUtilsTest.java b/src/test/java/camp/woowak/lab/web/api/utils/APIUtilsTest.java new file mode 100644 index 00000000..a421a8fa --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/api/utils/APIUtilsTest.java @@ -0,0 +1,73 @@ +package camp.woowak.lab.web.api.utils; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import com.fasterxml.jackson.core.JsonProcessingException; + +@DisplayName("APIUtils 클래스") +class APIUtilsTest { + @Nested + @DisplayName("of메서드의") + class OfTest { + @Nested + @DisplayName("HttpStatus 값과 Data를 파라미터로 받는 메서드는") + class ParamWithHttpStatusAndData { + @Test + @DisplayName("data와 status를 가지는 APIResponse를 생성할 수 있다.") + void APIResponseWithHttpStatusAndData() throws JsonProcessingException { + //given + HttpStatus status = HttpStatus.OK; + String message = "hello world"; + + //when + ResponseEntity> apiResponse = APIUtils.of(status, message); + + //then + assertThat(apiResponse.getStatusCode()).isEqualTo(status); + assertThat(apiResponse.getBody().getData()).isEqualTo(message); + assertThat(apiResponse.getBody().getStatus()).isEqualTo(status.value()); + } + + @Test + @DisplayName("data가 객체인 경우도 APIResponse를 생성할 수 있다.") + void APIResponseWithObjectData() throws JsonProcessingException { + //given + HttpStatus status = HttpStatus.OK; + Example example = new Example(27, "Hyeon-Uk"); + + //when + ResponseEntity> apiResponse = APIUtils.of(status, example); + + //then + assertThat(apiResponse.getStatusCode()).isEqualTo(status); + assertThat(apiResponse.getBody().getData()).isEqualTo(example); + assertThat(apiResponse.getBody().getStatus()).isEqualTo(status.value()); + } + + private class Example { + int age; + String name; + + public Example(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + } + } + + } +} \ No newline at end of file