diff --git a/.idea/misc.xml b/.idea/misc.xml
index 1af07c1..fd57316 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,4 +3,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 692c5f0..7016f02 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,7 +20,9 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
runtimeOnly 'com.mysql:mysql-connector-j'
diff --git a/src/main/java/ject/official_qr_checkin_server/common/exception/BusinessException.java b/src/main/java/ject/official_qr_checkin_server/common/exception/BusinessException.java
new file mode 100644
index 0000000..b285c46
--- /dev/null
+++ b/src/main/java/ject/official_qr_checkin_server/common/exception/BusinessException.java
@@ -0,0 +1,23 @@
+package ject.official_qr_checkin_server.common.exception;
+
+import java.io.Serial;
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private final ErrorCode errorCode;
+
+ public BusinessException(ErrorCode errorCode) {
+ this(errorCode, errorCode.getMessage());
+ }
+
+ public BusinessException(ErrorCode errorCode, String message) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+}
diff --git a/src/main/java/ject/official_qr_checkin_server/common/exception/ErrorCode.java b/src/main/java/ject/official_qr_checkin_server/common/exception/ErrorCode.java
new file mode 100644
index 0000000..e42dfba
--- /dev/null
+++ b/src/main/java/ject/official_qr_checkin_server/common/exception/ErrorCode.java
@@ -0,0 +1,13 @@
+package ject.official_qr_checkin_server.common.exception;
+
+import java.io.Serializable;
+import org.springframework.http.HttpStatus;
+
+public interface ErrorCode extends Serializable {
+
+ HttpStatus getHttpStatus();
+
+ String getCode();
+
+ String getMessage();
+}
diff --git a/src/main/java/ject/official_qr_checkin_server/common/exception/GlobalErrorCode.java b/src/main/java/ject/official_qr_checkin_server/common/exception/GlobalErrorCode.java
new file mode 100644
index 0000000..f481db3
--- /dev/null
+++ b/src/main/java/ject/official_qr_checkin_server/common/exception/GlobalErrorCode.java
@@ -0,0 +1,67 @@
+package ject.official_qr_checkin_server.common.exception;
+
+import org.springframework.http.HttpStatus;
+
+public enum GlobalErrorCode implements ErrorCode {
+
+ UNSUPPORTED_PARAMETER_TYPE(
+ HttpStatus.BAD_REQUEST,
+ "GLOBAL-001",
+ "요청 파라미터의 형식이 올바르지 않습니다."
+ ),
+ MISSING_REQUEST_PARAMETER(
+ HttpStatus.BAD_REQUEST,
+ "GLOBAL-002",
+ "필수 요청 파라미터가 누락되었습니다."
+ ),
+ MISSING_REQUEST_BODY(
+ HttpStatus.BAD_REQUEST,
+ "GLOBAL-003",
+ "요청 본문이 누락되었거나 형식이 올바르지 않습니다."
+ ),
+ VALIDATION_FAILED(
+ HttpStatus.BAD_REQUEST,
+ "GLOBAL-004",
+ "요청 값 검증에 실패했습니다."
+ ),
+ METHOD_NOT_ALLOWED(
+ HttpStatus.METHOD_NOT_ALLOWED,
+ "GLOBAL-005",
+ "지원하지 않는 HTTP 메서드입니다."
+ ),
+ RESOURCE_NOT_FOUND(
+ HttpStatus.NOT_FOUND,
+ "GLOBAL-006",
+ "요청한 리소스를 찾을 수 없습니다."
+ ),
+ INTERNAL_SERVER_ERROR(
+ HttpStatus.INTERNAL_SERVER_ERROR,
+ "GLOBAL-999",
+ "서버 내부 오류가 발생했습니다."
+ );
+
+ private final HttpStatus httpStatus;
+ private final String code;
+ private final String message;
+
+ GlobalErrorCode(HttpStatus httpStatus, String code, String message) {
+ this.httpStatus = httpStatus;
+ this.code = code;
+ this.message = message;
+ }
+
+ @Override
+ public HttpStatus getHttpStatus() {
+ return httpStatus;
+ }
+
+ @Override
+ public String getCode() {
+ return code;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/main/java/ject/official_qr_checkin_server/common/exception/GlobalExceptionHandler.java b/src/main/java/ject/official_qr_checkin_server/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..7907976
--- /dev/null
+++ b/src/main/java/ject/official_qr_checkin_server/common/exception/GlobalExceptionHandler.java
@@ -0,0 +1,126 @@
+package ject.official_qr_checkin_server.common.exception;
+
+import java.util.List;
+import java.util.Objects;
+import ject.official_qr_checkin_server.common.response.ApiResponse;
+import ject.official_qr_checkin_server.common.response.ErrorResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.HandlerMethodValidationException;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.servlet.resource.NoResourceFoundException;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ @ExceptionHandler(BusinessException.class)
+ ResponseEntity>> handleBusinessException(BusinessException exception) {
+ ErrorCode errorCode = exception.getErrorCode();
+ logKnownException(exception, errorCode);
+ return createResponse(ErrorResponse.of(errorCode, exception.getMessage()));
+ }
+
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ ResponseEntity>> handleMethodArgumentTypeMismatch(
+ MethodArgumentTypeMismatchException exception
+ ) {
+ return handleKnownException(exception, GlobalErrorCode.UNSUPPORTED_PARAMETER_TYPE);
+ }
+
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ ResponseEntity>> handleMissingRequestParameter(
+ MissingServletRequestParameterException exception
+ ) {
+ return handleKnownException(exception, GlobalErrorCode.MISSING_REQUEST_PARAMETER);
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ ResponseEntity>> handleMessageNotReadable(
+ HttpMessageNotReadableException exception
+ ) {
+ return handleKnownException(exception, GlobalErrorCode.MISSING_REQUEST_BODY);
+ }
+
+ @ExceptionHandler({
+ MethodArgumentNotValidException.class,
+ HandlerMethodValidationException.class
+ })
+ ResponseEntity>> handleValidationException(Exception exception) {
+ GlobalErrorCode errorCode = GlobalErrorCode.VALIDATION_FAILED;
+ logKnownException(exception, errorCode);
+
+ List messages = switch (exception) {
+ case MethodArgumentNotValidException methodArgumentNotValidException ->
+ extractMessages(methodArgumentNotValidException.getAllErrors(), errorCode);
+ case HandlerMethodValidationException handlerMethodValidationException ->
+ extractMessages(handlerMethodValidationException.getAllErrors(), errorCode);
+ default -> List.of(errorCode.getMessage());
+ };
+
+ return createResponse(ErrorResponse.of(errorCode, messages));
+ }
+
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ ResponseEntity>> handleMethodNotSupported(
+ HttpRequestMethodNotSupportedException exception
+ ) {
+ return handleKnownException(exception, GlobalErrorCode.METHOD_NOT_ALLOWED);
+ }
+
+ @ExceptionHandler(NoResourceFoundException.class)
+ ResponseEntity>> handleResourceNotFound(NoResourceFoundException exception) {
+ return handleKnownException(exception, GlobalErrorCode.RESOURCE_NOT_FOUND);
+ }
+
+ @ExceptionHandler(Exception.class)
+ ResponseEntity>> handleUnexpectedException(Exception exception) {
+ GlobalErrorCode errorCode = GlobalErrorCode.INTERNAL_SERVER_ERROR;
+ log.error("Unhandled exception. responseCode={}", errorCode.getCode(), exception);
+ return createResponse(ErrorResponse.of(errorCode));
+ }
+
+ private ResponseEntity>> handleKnownException(
+ Exception exception,
+ ErrorCode errorCode
+ ) {
+ logKnownException(exception, errorCode);
+ return createResponse(ErrorResponse.of(errorCode));
+ }
+
+ private List extractMessages(
+ List extends MessageSourceResolvable> errors,
+ ErrorCode fallbackErrorCode
+ ) {
+ List messages = errors.stream()
+ .map(MessageSourceResolvable::getDefaultMessage)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+
+ return messages.isEmpty() ? List.of(fallbackErrorCode.getMessage()) : messages;
+ }
+
+ private ResponseEntity>> createResponse(ErrorResponse errorResponse) {
+ return ResponseEntity
+ .status(errorResponse.httpStatus())
+ .body(errorResponse.toApiResponse());
+ }
+
+ private void logKnownException(Exception exception, ErrorCode errorCode) {
+ log.warn(
+ "Handled exception. type={}, responseCode={}",
+ exception.getClass().getSimpleName(),
+ errorCode.getCode()
+ );
+ }
+}
diff --git a/src/main/java/ject/official_qr_checkin_server/common/response/ApiResponse.java b/src/main/java/ject/official_qr_checkin_server/common/response/ApiResponse.java
new file mode 100644
index 0000000..d6b8298
--- /dev/null
+++ b/src/main/java/ject/official_qr_checkin_server/common/response/ApiResponse.java
@@ -0,0 +1,20 @@
+package ject.official_qr_checkin_server.common.response;
+
+import java.time.Instant;
+
+public record ApiResponse(
+ String status,
+ T data,
+ Instant timestamp
+) {
+
+ private static final String SUCCESS_STATUS = "SUCCESS";
+
+ public static ApiResponse success(T data) {
+ return new ApiResponse<>(SUCCESS_STATUS, data, Instant.now());
+ }
+
+ public static ApiResponse error(String status, T data) {
+ return new ApiResponse<>(status, data, Instant.now());
+ }
+}
diff --git a/src/main/java/ject/official_qr_checkin_server/common/response/ApiResponseBodyAdvice.java b/src/main/java/ject/official_qr_checkin_server/common/response/ApiResponseBodyAdvice.java
new file mode 100644
index 0000000..f784d86
--- /dev/null
+++ b/src/main/java/ject/official_qr_checkin_server/common/response/ApiResponseBodyAdvice.java
@@ -0,0 +1,71 @@
+package ject.official_qr_checkin_server.common.response;
+
+import java.util.List;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ProblemDetail;
+import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+@RestControllerAdvice
+public class ApiResponseBodyAdvice implements ResponseBodyAdvice