Skip to content

[그리디] 김지우 Spring Core (배포) 7,8,9 단계 제출합니다. #178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: ji-woo-kim
Choose a base branch
from
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 🔓 방탈출 예약 서비스

<br>
<img width="800" height="413" alt="image" src="https://github.com/user-attachments/assets/9ce9fd16-4341-46d0-9f62-750021db4dd6" />
<img width="800" height="248" alt="image" src="https://github.com/user-attachments/assets/46d1c290-90ad-450c-8a0b-d05cb32ae7af" />
<br>

## 🚀 주요 기능

1. **로그인**: 가입한 이메일과 비밀번호로 로그인할 수 있습니다.

2. **예약**: '예약' 페이지에서 원하는 날짜, 테마, 시간을 선택하고 '예약하기' 버튼을 누르면 예약이 완료됩니다.

3. **예약 확인 및 취소**: '내 예약' 페이지에서 예약 내역을 확인하거나, 원치 않는 예약을 취소할 수 있습니다.

4. **예약 대기 신청**: 예약 대기를 신청하면, 취소 자리가 생겼을 때 우선적으로 예약할 기회를 드립니다.
69 changes: 69 additions & 0 deletions src/main/java/auth/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Arrays;
import java.util.Date;

public class JwtUtils {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JwtUtils에 Component를 제거한 이유가 있나요?
auth라는 roomspace와 동등한 레벨의 패키지로 분리한 이유도 궁금합니다!

Copy link
Author

@Ji-Woo-Kim Ji-Woo-Kim Jul 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 방식으로 구현한 이유는 우선 미션 요구사항과 주어진 테스트를 통과시키기 위해서입니다.

@Test
void 칠단계() {
    Component componentAnnotation = JwtUtils.class.getAnnotation(Component.class);
    assertThat(componentAnnotation).isNull();
}

요구사항 해결 방향성
JWT 관련 로직으로는 토큰 생성과 토큰 값 조회 기능이 있습니다.
해당 로직을 하나의 클래스로 이동 시킨 후 roomescape 외부 패키지로 이동하세요.
이 때 @Configuraion과 @bean이 사용됩니다.

주어진 테스트코드에서 JwtUtils 클래스가 @Component 어노테이션을 가지고 있지 않아야 통과하도록 짜여져 있었습니다. 또한, 미션 요구사항에 따라 roomescape와 동등한 레벨의 패키지를 생성하였고@Configuration@Bean 사용에 대한 해결 방향성을 참고하여 AuthConfig를 통해 secretKey를 주입받아 JwtUtils Bean을 생성하는 방식으로 구현했습니다.

단순히 미션의 요구사항만을 충족하고 넘어가기엔 아쉬운 것 같아
왜 이렇게 구현을 해야하는지, 이 미션을 통해 무엇을 학습해봐야 하는지 생각해보았는데요!
자동으로 빈을 등록하는 방식(@component 사용)과 수동으로 등록하는 방식의 차이를 경험해보고
서비스 로직과 인증 로직을 분리하는 설계를 학습해보라는 의도가 있는 것이 아닌가 싶습니다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가적인 고민까지 좋습니다!
jwtUtils는 어떤 애플리케이션에서 사용 할 수 있는 코드, 일종의 라이브러리로 간주 하였다면
AuthConfig.java는 roomesacpe에 포함 되어야 할 것 같아요.
roomescape에서 jwtUtils를 빈으로 등록하는 방식으로요!
실제로 외부 라이브러리를 빈에 등록 시킬 때, @Bean을 자주 활용 합니다.


private final Key key;

public JwtUtils(String secretKey) {
this.key = new SecretKeySpec(secretKey.getBytes(), SignatureAlgorithm.HS256.getJcaName());
}

public String createToken(String id, String role) {
Claims claims = Jwts.claims().setSubject(id);
claims.put("role", role);
Date now = new Date();
Date validity = new Date(now.getTime() + 3600000);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

private Claims getClaims(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return claims.getBody();
} catch (JwtException | IllegalArgumentException e) {
throw new IllegalArgumentException("유효하지 않은 토큰입니다.");
}
}

public String getSubject(String token) {
return getClaims(token).getSubject();
}

public String getRole(String token) {
return getClaims(token).get("role", String.class);
}

public String getTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
}
}
11 changes: 11 additions & 0 deletions src/main/java/auth/annotation/Login.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package auth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
25 changes: 22 additions & 3 deletions src/main/java/roomescape/ExceptionController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
package roomescape;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 roomescape.auth.exception.UnauthenticatedException;

@ControllerAdvice
public class ExceptionController {

private static final Logger log = LoggerFactory.getLogger(ExceptionController.class);

@ExceptionHandler(Exception.class)
public ResponseEntity<Void> handleRuntimeException(Exception e) {
e.printStackTrace();
return ResponseEntity.badRequest().build();
public ResponseEntity<String> handleRuntimeException(Exception e) {
log.error("예상치 못한 예외가 발생했습니다.", e);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

더 좋은 방식의 예외 로그 출력이에요👍👍
아래 다른 ExceptionHandler에도 발생한 예외에 대해서 로그를 남기면 좋을 것 같아요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오류를 잡기 위해서 리팩토링 했던 부분이라 미처 다른 부분은 신경을 못 썼네요 🥲
다른 부분도 로그 출력하도록 수정해두었습니다!

return ResponseEntity.internalServerError().body("서버 내부 오류가 발생했습니다.");
}

@ExceptionHandler(UnauthenticatedException.class)
public ResponseEntity<String> handleUnauthenticatedException(UnauthenticatedException e) {
log.error("인증되지 않은 요청입니다.", e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("잘못된 요청: {}", e.getMessage(), e);
return ResponseEntity.badRequest().body(e.getMessage());
}
}
2 changes: 1 addition & 1 deletion src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@SpringBootApplication(scanBasePackages = {"roomescape", "auth"})
public class RoomescapeApplication {
public static void main(String[] args) {
SpringApplication.run(RoomescapeApplication.class, args);
Expand Down
31 changes: 10 additions & 21 deletions src/main/java/roomescape/auth/AdminInterceptor.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
package roomescape.auth;

import jakarta.servlet.http.Cookie;
import auth.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Arrays;
import java.util.Optional;

@Component
public class AdminInterceptor implements HandlerInterceptor {

private final JwtTokenProvider jwtTokenProvider;
private final JwtUtils jwtUtils;

public AdminInterceptor(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
public AdminInterceptor(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Optional<Cookie> tokenCookie = findTokenCookie(request.getCookies());
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
String token = jwtUtils.getTokenFromCookie(request);

if (tokenCookie.isEmpty()) {
if (token == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}

try {
String token = tokenCookie.get().getValue();
String role = jwtTokenProvider.getRole(token);
String role = jwtUtils.getRole(token);

if (!"ADMIN".equals(role)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
Expand All @@ -41,13 +39,4 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons

return true;
}

private Optional<Cookie> findTokenCookie(Cookie[] cookies) {
if (cookies == null) {
return Optional.empty();
}
return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.findFirst();
}
}
51 changes: 0 additions & 51 deletions src/main/java/roomescape/auth/JwtTokenProvider.java

This file was deleted.

18 changes: 11 additions & 7 deletions src/main/java/roomescape/auth/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package roomescape.auth;

import auth.JwtUtils;
import auth.annotation.Login;
import roomescape.auth.dto.LoginMember;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
Expand All @@ -8,22 +11,26 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.auth.dto.LoginCheckResponse;
import roomescape.auth.dto.LoginMember;
import roomescape.auth.dto.LoginRequest;
import roomescape.member.Member;
import roomescape.member.MemberService;

@RestController
public class LoginController {

private final MemberService memberService;
private final JwtUtils jwtUtils;

public LoginController(MemberService memberService) {
public LoginController(MemberService memberService, JwtUtils jwtUtils) {
this.memberService = memberService;
this.jwtUtils = jwtUtils;
}

@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody LoginRequest request, HttpServletResponse response) {
String token = memberService.login(request);
Member member = memberService.login(request);

String token = jwtUtils.createToken(String.valueOf(member.getId()), member.getRole());

Cookie cookie = new Cookie("token", token);
cookie.setHttpOnly(true);
Expand All @@ -45,10 +52,7 @@ public ResponseEntity<Void> logout(HttpServletResponse response) {
}

@GetMapping("/login/check")
public ResponseEntity<LoginCheckResponse> checkLogin(LoginMember loginMember) {
if (loginMember == null) {
return ResponseEntity.status(401).build();
}
public ResponseEntity<LoginCheckResponse> checkLogin(@Login LoginMember loginMember) {
return ResponseEntity.ok(new LoginCheckResponse(loginMember.getName()));
}
}
67 changes: 0 additions & 67 deletions src/main/java/roomescape/auth/LoginMemberArgumentResolver.java

This file was deleted.

Loading