diff --git "a/.run/MissionStepTest.\354\202\274\353\213\250\352\263\204.run.xml" "b/.run/MissionStepTest.\354\202\274\353\213\250\352\263\204.run.xml"
new file mode 100644
index 000000000..80b29a46e
--- /dev/null
+++ "b/.run/MissionStepTest.\354\202\274\353\213\250\352\263\204.run.xml"
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ true
+ false
+ true
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 8d52aebc6..f6df4186f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,7 +15,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
- implementation 'org.springframework.boot:spring-boot-starter-jdbc'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'
@@ -26,7 +26,12 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
+ implementation 'com.h2database:h2'
runtimeOnly 'com.h2database:h2'
+
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
+ implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
+
}
test {
diff --git a/src/main/java/jwt/JwtConfiguration.java b/src/main/java/jwt/JwtConfiguration.java
new file mode 100644
index 000000000..ad7242bba
--- /dev/null
+++ b/src/main/java/jwt/JwtConfiguration.java
@@ -0,0 +1,17 @@
+package jwt;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class JwtConfiguration {
+
+ @Value("${jwt.secret}")
+ private String secretKey;
+
+ @Bean
+ public JwtUtils jwtUtils() {
+ return new JwtUtils();
+ }
+}
diff --git a/src/main/java/jwt/JwtService.java b/src/main/java/jwt/JwtService.java
new file mode 100644
index 000000000..cfb44da58
--- /dev/null
+++ b/src/main/java/jwt/JwtService.java
@@ -0,0 +1,35 @@
+package jwt;
+
+import io.jsonwebtoken.Claims;
+import org.springframework.stereotype.Service;
+import roomescape.member.Member;
+import roomescape.member.MemberRepository;
+
+@Service
+public class JwtService {
+ private final JwtUtils jwtUtils;
+ private final MemberRepository memberRepository;
+
+ public JwtService(JwtUtils jwtUtils, MemberRepository memberRepository) {
+ this.jwtUtils = jwtUtils;
+ this.memberRepository = memberRepository;
+ }
+
+ public Long getUserIdFromToken(String token) {
+ Claims claims = jwtUtils.getClaimsFromToken(token);
+ try {
+ return Long.valueOf(claims.getSubject());
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Claims에서 User ID를 추출할 수 없습니다.", e);
+ }
+ }
+
+ public Member getMemberFromToken(String token) {
+ Long userId = getUserIdFromToken(token);
+ Member member = memberRepository.findById(userId).get();
+ if (member == null) {
+ throw new IllegalArgumentException("토큰으로부터 유저를 찾을 수 없습니다.");
+ }
+ return member;
+ }
+}
diff --git a/src/main/java/jwt/JwtUtils.java b/src/main/java/jwt/JwtUtils.java
new file mode 100644
index 000000000..61d465487
--- /dev/null
+++ b/src/main/java/jwt/JwtUtils.java
@@ -0,0 +1,49 @@
+package jwt;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.springframework.beans.factory.annotation.Value;
+import roomescape.member.Role;
+
+import javax.crypto.spec.SecretKeySpec;
+import java.security.Key;
+import java.util.Date;
+
+public class JwtUtils {
+ private static final long EXPIRATION_TIME = 86400000;
+ private String secretKey;
+
+ @Value("${jwt.secret}")
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ private Key generateKey() {
+ return new SecretKeySpec(secretKey.getBytes(), SignatureAlgorithm.HS256.getJcaName());
+ }
+
+ public String generateToken(Long userId, Role role) {
+ Key key = generateKey();
+ return Jwts.builder()
+ .setSubject(String.valueOf(userId))
+ .claim("role", role.name())
+ .setIssuedAt(new Date())
+ .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
+ .signWith(key, SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ public Claims getClaimsFromToken(String token) {
+ try {
+ Key key = generateKey();
+ return Jwts.parserBuilder()
+ .setSigningKey(key)
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("유효하지 않은 토큰입니다.", e);
+ }
+ }
+}
diff --git a/src/main/java/roomescape/ExceptionController.java b/src/main/java/roomescape/ExceptionController.java
index 4e2450f9e..e452ff0cb 100644
--- a/src/main/java/roomescape/ExceptionController.java
+++ b/src/main/java/roomescape/ExceptionController.java
@@ -1,14 +1,33 @@
package roomescape;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.HashMap;
+import java.util.Map;
@ControllerAdvice
public class ExceptionController {
+
+ @ExceptionHandler(ResponseStatusException.class)
+ public ResponseEntity handleResponseStatusException(ResponseStatusException e) {
+ return ResponseEntity.status(e.getStatusCode()).build();
+ }
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResponseEntity