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 extends Annotation>[] 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