From eaef783553bf42d91427ea03a269ed5210578327 Mon Sep 17 00:00:00 2001 From: Jiwoo Date: Wed, 16 Jul 2025 02:43:24 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=207=EB=8B=A8=EA=B3=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84-JWT=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20roome?= =?UTF-8?q?scape=20=EC=99=B8=EB=B6=80=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/auth/AuthConfig.java | 17 +++++ src/main/java/auth/JwtUtils.java | 47 +++++++++++++ src/main/java/auth/annotation/Login.java | 11 +++ .../auth/dto/LoginMember.java | 4 +- .../auth/ui/LoginUserArgumentResolver.java | 62 +++++++++++++++++ .../roomescape/RoomescapeApplication.java | 2 +- .../roomescape/auth/AdminInterceptor.java | 12 ++-- .../roomescape/auth/JwtTokenProvider.java | 51 -------------- .../java/roomescape/auth/LoginController.java | 13 +++- .../auth/LoginMemberArgumentResolver.java | 67 ------------------- .../java/roomescape/config/WebConfig.java | 15 ++--- .../java/roomescape/member/MemberService.java | 9 +-- .../reservation/ReservationController.java | 9 +-- .../reservation/ReservationService.java | 8 +-- .../roomescape/waiting/WaitingController.java | 5 +- .../roomescape/waiting/WaitingService.java | 5 +- src/main/resources/application.properties | 2 +- src/test/java/roomescape/MissionStepTest.java | 44 ++++++++++++ 18 files changed, 228 insertions(+), 155 deletions(-) create mode 100644 src/main/java/auth/AuthConfig.java create mode 100644 src/main/java/auth/JwtUtils.java create mode 100644 src/main/java/auth/annotation/Login.java rename src/main/java/{roomescape => }/auth/dto/LoginMember.java (64%) create mode 100644 src/main/java/auth/ui/LoginUserArgumentResolver.java delete mode 100644 src/main/java/roomescape/auth/JwtTokenProvider.java delete mode 100644 src/main/java/roomescape/auth/LoginMemberArgumentResolver.java diff --git a/src/main/java/auth/AuthConfig.java b/src/main/java/auth/AuthConfig.java new file mode 100644 index 000000000..192720433 --- /dev/null +++ b/src/main/java/auth/AuthConfig.java @@ -0,0 +1,17 @@ +package auth; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AuthConfig { + + @Value("${jwt.secret-key}") + private String secretKey; + + @Bean + public JwtUtils jwtUtils() { + return new JwtUtils(secretKey); + } +} diff --git a/src/main/java/auth/JwtUtils.java b/src/main/java/auth/JwtUtils.java new file mode 100644 index 000000000..7a11c0c26 --- /dev/null +++ b/src/main/java/auth/JwtUtils.java @@ -0,0 +1,47 @@ +package auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.util.Date; + +public class JwtUtils { + + private final Key key; + + public JwtUtils(String secretKey) { + this.key = new SecretKeySpec(secretKey.getBytes(), SignatureAlgorithm.HS256.getJcaName()); + } + + public String createToken(String id, String name, String email, String role) { + Claims claims = Jwts.claims().setSubject(id); + claims.put("name", name); + claims.put("email", email); + claims.put("role", role); + Date now = new Date(); + Date validity = new Date(now.getTime() + 3600); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public Claims getClaims(String token) { + try { + Jws claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + return claims.getBody(); + } catch (JwtException | IllegalArgumentException e) { + throw new IllegalArgumentException("유효하지 않은 토큰입니다."); + } + } +} diff --git a/src/main/java/auth/annotation/Login.java b/src/main/java/auth/annotation/Login.java new file mode 100644 index 000000000..d6ee3111d --- /dev/null +++ b/src/main/java/auth/annotation/Login.java @@ -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 { +} diff --git a/src/main/java/roomescape/auth/dto/LoginMember.java b/src/main/java/auth/dto/LoginMember.java similarity index 64% rename from src/main/java/roomescape/auth/dto/LoginMember.java rename to src/main/java/auth/dto/LoginMember.java index 2b4b10eab..211c49df7 100644 --- a/src/main/java/roomescape/auth/dto/LoginMember.java +++ b/src/main/java/auth/dto/LoginMember.java @@ -1,11 +1,11 @@ -package roomescape.auth.dto; +package auth.dto; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor -public class LoginMember { // 로그인한 사용자 정보를 담는 DTO +public class LoginMember { private Long id; private String name; diff --git a/src/main/java/auth/ui/LoginUserArgumentResolver.java b/src/main/java/auth/ui/LoginUserArgumentResolver.java new file mode 100644 index 000000000..2ad4887e7 --- /dev/null +++ b/src/main/java/auth/ui/LoginUserArgumentResolver.java @@ -0,0 +1,62 @@ +package auth.ui; + +import auth.JwtUtils; +import auth.annotation.Login; +import auth.dto.LoginMember; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.Arrays; +import java.util.Optional; + +@Component +public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtUtils jwtUtils; + + public LoginUserArgumentResolver(JwtUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Login.class) + && parameter.getParameterType().equals(LoginMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + Optional tokenCookie = findTokenCookie(request.getCookies()); + if (tokenCookie.isEmpty()) { + return null; + } + String token = tokenCookie.get().getValue(); + + Claims claims = jwtUtils.getClaims(token); + Long id = Long.parseLong(claims.getSubject()); + String name = claims.get("name", String.class); + String email = claims.get("email", String.class); + String role = claims.get("role", String.class); + + return new LoginMember(id, name, email, role); + } + + private Optional findTokenCookie(Cookie[] cookies) { + if (cookies == null) { + return Optional.empty(); + } + return Arrays.stream(cookies) + .filter(cookie -> "token".equals(cookie.getName())) + .findFirst(); + } +} diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 2ca0f743f..f63d4ca0f 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -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); diff --git a/src/main/java/roomescape/auth/AdminInterceptor.java b/src/main/java/roomescape/auth/AdminInterceptor.java index 289db20c5..191a3e2c0 100644 --- a/src/main/java/roomescape/auth/AdminInterceptor.java +++ b/src/main/java/roomescape/auth/AdminInterceptor.java @@ -1,5 +1,7 @@ package roomescape.auth; +import auth.JwtUtils; +import io.jsonwebtoken.Claims; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -12,10 +14,10 @@ @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 @@ -29,7 +31,9 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons try { String token = tokenCookie.get().getValue(); - String role = jwtTokenProvider.getRole(token); + Claims claims = jwtUtils.getClaims(token); + String role = claims.get("role", String.class); + if (!"ADMIN".equals(role)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; diff --git a/src/main/java/roomescape/auth/JwtTokenProvider.java b/src/main/java/roomescape/auth/JwtTokenProvider.java deleted file mode 100644 index 58ec0a070..000000000 --- a/src/main/java/roomescape/auth/JwtTokenProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -package roomescape.auth; - -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import roomescape.member.Member; - -import javax.crypto.SecretKey; - -@Component -public class JwtTokenProvider { - - private SecretKey secretKey; - - @Value("${roomescape.auth.jwt.secret}") - private String secret; - - @PostConstruct - public void init() { - this.secretKey = Keys.hmacShaKeyFor(secret.getBytes()); - } - - public String createToken(Member member) { - return Jwts.builder() - .setSubject(member.getId().toString()) - .claim("name", member.getName()) - .claim("role", member.getRole()) - .signWith(secretKey) - .compact(); - } - - public String getSubject(String token) { - return Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody() - .getSubject(); - } - - public String getRole(String token) { - return Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody() - .get("role", String.class); - } -} diff --git a/src/main/java/roomescape/auth/LoginController.java b/src/main/java/roomescape/auth/LoginController.java index c3c67fc3b..1d8afcecb 100644 --- a/src/main/java/roomescape/auth/LoginController.java +++ b/src/main/java/roomescape/auth/LoginController.java @@ -1,5 +1,8 @@ package roomescape.auth; +import auth.JwtUtils; +import auth.annotation.Login; +import auth.dto.LoginMember; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; @@ -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 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.getName(), member.getEmail(), member.getRole()); Cookie cookie = new Cookie("token", token); cookie.setHttpOnly(true); diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java deleted file mode 100644 index adefee7c2..000000000 --- a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java +++ /dev/null @@ -1,67 +0,0 @@ -package roomescape.auth; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; -import roomescape.auth.dto.LoginMember; -import roomescape.auth.exception.UnauthenticatedException; -import roomescape.member.Member; -import roomescape.member.MemberService; - -import java.util.Arrays; - -@Component -public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { - - private final JwtTokenProvider jwtTokenProvider; - private final MemberService memberService; - - public LoginMemberArgumentResolver(JwtTokenProvider jwtTokenProvider, MemberService memberService) { - this.jwtTokenProvider = jwtTokenProvider; - this.memberService = memberService; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - // 컨트롤러 메서드의 파라미터가 LoginMember 타입일 때 이 리졸버를 사용하도록 설정 - return parameter.getParameterType().equals(LoginMember.class); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - if (request == null) { - throw new UnauthenticatedException("요청 정보를 찾을 수 없습니다."); - } - - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - return null; - } - - String token = Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals("token")) - .map(Cookie::getValue) - .findFirst() - .orElse(null); - - if (token == null) { - return null; // 토큰이 없으면 null 반환 - } - - try { - Long memberId = Long.valueOf(jwtTokenProvider.getSubject(token)); - Member member = memberService.findById(memberId); - return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); - } catch (Exception e) { - // 토큰이 유효하지 않은 경우 등 - throw new UnauthenticatedException("유효하지 않은 토큰입니다.", e); - } - } -} diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java index 91c824905..b8987a87e 100644 --- a/src/main/java/roomescape/config/WebConfig.java +++ b/src/main/java/roomescape/config/WebConfig.java @@ -1,34 +1,33 @@ package roomescape.config; +import auth.ui.LoginUserArgumentResolver; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import roomescape.auth.AdminInterceptor; -import roomescape.auth.LoginMemberArgumentResolver; import java.util.List; @Configuration public class WebConfig implements WebMvcConfigurer { - private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final LoginUserArgumentResolver loginUserArgumentResolver; private final AdminInterceptor adminInterceptor; - public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, - AdminInterceptor adminInterceptor) { - this.loginMemberArgumentResolver = loginMemberArgumentResolver; + public WebConfig(LoginUserArgumentResolver loginUserArgumentResolver, AdminInterceptor adminInterceptor) { + this.loginUserArgumentResolver = loginUserArgumentResolver; this.adminInterceptor = adminInterceptor; } @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(loginMemberArgumentResolver); + resolvers.add(loginUserArgumentResolver); } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(adminInterceptor) // 등록할 인터셉터 지정 - .addPathPatterns("/admin/**"); // 인터셉터를 적용할 URL 패턴 지정 + registry.addInterceptor(adminInterceptor) + .addPathPatterns("/admin/**"); } } diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index 9244e5ac1..1a45f3e71 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -2,7 +2,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import roomescape.auth.JwtTokenProvider; import roomescape.auth.dto.LoginRequest; import roomescape.member.dto.MemberRequest; import roomescape.member.dto.MemberResponse; @@ -11,11 +10,9 @@ public class MemberService { private final MemberRepository memberRepository; - private final JwtTokenProvider jwtTokenProvider; - public MemberService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { + public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; - this.jwtTokenProvider = jwtTokenProvider; } @Transactional @@ -27,7 +24,7 @@ public MemberResponse createMember(MemberRequest memberRequest) { } @Transactional(readOnly = true) - public String login(LoginRequest request) { + public Member login(LoginRequest request) { // 반환 타입을 String(토큰)에서 Member로 변경 Member member = memberRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new IllegalArgumentException("이메일 또는 비밀번호가 일치하지 않습니다.")); @@ -35,7 +32,7 @@ public String login(LoginRequest request) { throw new IllegalArgumentException("이메일 또는 비밀번호가 일치하지 않습니다."); } - return jwtTokenProvider.createToken(member); + return member; } public Member findById(Long id) { diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index 9fa4cf0c6..e555cf0ea 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -1,5 +1,7 @@ package roomescape.reservation; +import auth.annotation.Login; +import auth.dto.LoginMember; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -7,7 +9,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.auth.dto.LoginMember; import roomescape.reservation.dto.MyReservationResponse; import roomescape.reservation.dto.ReservationRequest; import roomescape.reservation.dto.ReservationResponse; @@ -30,7 +31,7 @@ public ResponseEntity> list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest request, LoginMember loginMember) { + public ResponseEntity create(@RequestBody ReservationRequest request, @Login LoginMember loginMember) { ReservationResponse reservation = reservationService.create(request, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); } @@ -48,8 +49,8 @@ public ResponseEntity delete(@PathVariable Long id) { } @GetMapping("/reservations-mine") - public ResponseEntity> findMyReservations(LoginMember loginMember) { - List myReservations = reservationService.findMyReservations(loginMember); + public ResponseEntity> findMyReservations(@Login LoginMember loginMember) { + List myReservations = reservationService.findMyReservations(loginMember.getId()); return ResponseEntity.ok(myReservations); } } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index 4aedc852c..1690e3095 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,8 +1,8 @@ package roomescape.reservation; +import auth.dto.LoginMember; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import roomescape.auth.dto.LoginMember; import roomescape.member.Member; import roomescape.member.MemberRepository; import roomescape.reservation.dto.MyReservationResponse; @@ -80,9 +80,9 @@ private Member determineReservationHolder(ReservationRequest request, Member log return loggedInUser; } - public List findMyReservations(LoginMember loginMember) { - List reservations = reservationRepository.findWithDetailsByMemberId(loginMember.getId()); - List waitings = waitingRepository.findWaitingsWithRankByMemberId(loginMember.getId()); + public List findMyReservations(Long memberId) { + List reservations = reservationRepository.findWithDetailsByMemberId(memberId); + List waitings = waitingRepository.findWaitingsWithRankByMemberId(memberId); Stream reservationResponses = reservations.stream() .map(r -> MyReservationResponse.from(r, r.getTheme(), r.getTime())); diff --git a/src/main/java/roomescape/waiting/WaitingController.java b/src/main/java/roomescape/waiting/WaitingController.java index d03329722..027e18a7c 100644 --- a/src/main/java/roomescape/waiting/WaitingController.java +++ b/src/main/java/roomescape/waiting/WaitingController.java @@ -1,5 +1,7 @@ package roomescape.waiting; +import auth.annotation.Login; +import auth.dto.LoginMember; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -7,7 +9,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import roomescape.auth.dto.LoginMember; import roomescape.waiting.dto.WaitingRequest; import roomescape.waiting.dto.WaitingResponse; @@ -24,7 +25,7 @@ public WaitingController(WaitingService waitingService) { } @PostMapping - public ResponseEntity createWaiting(@RequestBody WaitingRequest request, LoginMember loginMember) { + public ResponseEntity createWaiting(@RequestBody WaitingRequest request, @Login LoginMember loginMember) { WaitingResponse response = waitingService.create(request, loginMember); return ResponseEntity.created(URI.create("/waitings/" + response.getId())).body(response); } diff --git a/src/main/java/roomescape/waiting/WaitingService.java b/src/main/java/roomescape/waiting/WaitingService.java index cefaf5fa7..664484849 100644 --- a/src/main/java/roomescape/waiting/WaitingService.java +++ b/src/main/java/roomescape/waiting/WaitingService.java @@ -1,8 +1,9 @@ package roomescape.waiting; +import auth.annotation.Login; +import auth.dto.LoginMember; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import roomescape.auth.dto.LoginMember; import roomescape.auth.exception.UnauthenticatedException; import roomescape.member.Member; import roomescape.member.MemberRepository; @@ -34,7 +35,7 @@ public WaitingService(WaitingRepository waitingRepository, ReservationRepository } @Transactional - public WaitingResponse create(WaitingRequest request, LoginMember loginMember) { + public WaitingResponse create(WaitingRequest request, @Login LoginMember loginMember) { Member member = memberRepository.findById(loginMember.getId()) .orElseThrow(() -> new IllegalArgumentException("사용자 정보를 찾을 수 없습니다.")); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 83bf1bb20..392b89f22 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,4 @@ spring.jpa.ddl-auto=create-drop spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always -roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= \ No newline at end of file +jwt.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= \ No newline at end of file diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 8011fe97b..192cf1c5f 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -1,11 +1,16 @@ package roomescape; +import auth.JwtUtils; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; import roomescape.reservation.dto.MyReservationResponse; import roomescape.reservation.dto.ReservationResponse; @@ -43,6 +48,39 @@ private String createToken(String email, String password) { return response.response().getCookie("token"); } + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + jdbcTemplate.execute("DELETE FROM waiting"); + jdbcTemplate.execute("DELETE FROM reservation"); + jdbcTemplate.execute("DELETE FROM member"); + jdbcTemplate.execute("DELETE FROM theme"); + jdbcTemplate.execute("DELETE FROM time"); + + jdbcTemplate.execute("ALTER TABLE waiting ALTER COLUMN id RESTART WITH 1"); + jdbcTemplate.execute("ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1"); + jdbcTemplate.execute("ALTER TABLE member ALTER COLUMN id RESTART WITH 1"); + jdbcTemplate.execute("ALTER TABLE theme ALTER COLUMN id RESTART WITH 1"); + jdbcTemplate.execute("ALTER TABLE time ALTER COLUMN id RESTART WITH 1"); + + // Member (ID: 1=어드민, 2=브라운) + jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('어드민', 'admin@email.com', 'password', 'ADMIN')"); + jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('브라운', 'brown@email.com', 'password', 'USER')"); + + // Theme (ID: 1) + jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('공포 테마', '매우 무서운 테마입니다.', 'thumbnail.jpg')"); + + // Time (ID: 1) + jdbcTemplate.update("INSERT INTO time (time) VALUES ('10:00')"); + + // Reservation (어드민 계정으로 3개의 예약 생성 - 오단계 테스트용) + jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2025-01-01', 1, 1, 1)"); + jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2025-01-02', 1, 1, 1)"); + jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2025-01-03', 1, 1, 1)"); + } + @Test void 일단계() { String token = createToken("admin@email.com", "password"); @@ -155,4 +193,10 @@ private String createToken(String email, String password) { assertThat(status).isEqualTo("1번째 예약대기"); } + + @Test + void 칠단계() { + Component componentAnnotation = JwtUtils.class.getAnnotation(Component.class); + assertThat(componentAnnotation).isNull(); + } } From d74edeba84fba4d6690cf1e8b9cf5db802f026a7 Mon Sep 17 00:00:00 2001 From: Jiwoo Date: Wed, 16 Jul 2025 02:49:08 +0900 Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20=EB=B9=84=EB=B0=80=ED=82=A4?= =?UTF-8?q?=EA=B0=92=20=EC=A3=BC=EC=96=B4=EC=A7=84=20=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/auth/AuthConfig.java | 2 +- src/main/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/auth/AuthConfig.java b/src/main/java/auth/AuthConfig.java index 192720433..27ecdc67e 100644 --- a/src/main/java/auth/AuthConfig.java +++ b/src/main/java/auth/AuthConfig.java @@ -7,7 +7,7 @@ @Configuration public class AuthConfig { - @Value("${jwt.secret-key}") + @Value("${roomescape.auth.jwt.secret}") private String secretKey; @Bean diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 392b89f22..83bf1bb20 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,4 @@ spring.jpa.ddl-auto=create-drop spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always -jwt.secret-key=Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= \ No newline at end of file +roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= \ No newline at end of file From 6c7e15270a98951ff8f9de430d32a68360948a6e Mon Sep 17 00:00:00 2001 From: Jiwoo Date: Wed, 16 Jul 2025 18:33:45 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=208=EB=8B=A8=EA=B3=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20data.sql=20=EC=82=AD=EC=A0=9C=20-=20dataloader,?= =?UTF-8?q?=20testdataloader=20=EC=B6=94=EA=B0=80=20-=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=82=BD=EC=9E=85=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/config/DataLoader.java | 30 ++++++++++++ .../roomescape/config/TestDataLoader.java | 40 ++++++++++++++++ src/main/resources/data.sql | 15 ------ src/test/java/roomescape/JpaTest.java | 1 + src/test/java/roomescape/MissionStepTest.java | 48 +++++-------------- 5 files changed, 83 insertions(+), 51 deletions(-) create mode 100644 src/main/java/roomescape/config/DataLoader.java create mode 100644 src/main/java/roomescape/config/TestDataLoader.java delete mode 100644 src/main/resources/data.sql diff --git a/src/main/java/roomescape/config/DataLoader.java b/src/main/java/roomescape/config/DataLoader.java new file mode 100644 index 000000000..c817df913 --- /dev/null +++ b/src/main/java/roomescape/config/DataLoader.java @@ -0,0 +1,30 @@ +package roomescape.config; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Profile("!test") +@Component +public class DataLoader implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public DataLoader(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void run(String... args) throws Exception { + jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('어드민', 'admin@email.com', 'password', 'ADMIN')"); + + jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('공포', '매우 무서운 테마', 'https://i.imgur.com/1.jpg')"); + jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('코믹', '매우 웃긴 테마', 'https://i.imgur.com/2.jpg')"); + jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('어드벤처', '신나는 모험 테마', 'https://i.imgur.com/3.jpg')"); + + jdbcTemplate.update("INSERT INTO time (time) VALUES ('10:00')"); + jdbcTemplate.update("INSERT INTO time (time) VALUES ('13:00')"); + jdbcTemplate.update("INSERT INTO time (time) VALUES ('15:00')"); + } +} diff --git a/src/main/java/roomescape/config/TestDataLoader.java b/src/main/java/roomescape/config/TestDataLoader.java new file mode 100644 index 000000000..34a2121e6 --- /dev/null +++ b/src/main/java/roomescape/config/TestDataLoader.java @@ -0,0 +1,40 @@ +package roomescape.config; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Profile("test") +@Component +public class TestDataLoader implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public TestDataLoader(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void run(String... args) throws Exception { + // Member (ID: 1=어드민, 2=브라운, 3=클로이) + jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('어드민', 'admin@email.com', 'password', 'ADMIN')"); + jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('브라운', 'brown@email.com', 'password', 'USER')"); + jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('클로이', 'chloe@email.com', 'password', 'USER')"); + + // Theme + jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('공포', '매우 무서운 테마', 'https://i.imgur.com/1.jpg')"); + jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('코믹', '매우 웃긴 테마', 'https://i.imgur.com/2.jpg')"); + jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('어드벤처', '신나는 모험 테마', 'https://i.imgur.com/3.jpg')"); + + // Time + jdbcTemplate.update("INSERT INTO time (time) VALUES ('10:00')"); + jdbcTemplate.update("INSERT INTO time (time) VALUES ('13:00')"); + jdbcTemplate.update("INSERT INTO time (time) VALUES ('15:00')"); + + // Reservation + jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2024-03-01', 1, 1, 1)"); + jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2024-03-01', 1, 1, 1)"); + jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2024-03-01', 1, 1, 1)"); + } +} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql deleted file mode 100644 index de8f21cf9..000000000 --- a/src/main/resources/data.sql +++ /dev/null @@ -1,15 +0,0 @@ -INSERT INTO member (name, email, password, role) VALUES ('어드민', 'admin@email.com', 'password', 'ADMIN'); -INSERT INTO member (name, email, password, role) VALUES ('클로이', 'chloe@email.com', 'password', 'USER'); -INSERT INTO member (name, email, password, role) VALUES ('브라운', 'brown@email.com', 'password', 'USER'); - -INSERT INTO theme (name, description, thumbnail) VALUES ('공포', '매우 무서운 테마', 'https://i.imgur.com/1.jpg'); -INSERT INTO theme (name, description, thumbnail) VALUES ('코믹', '매우 웃긴 테마', 'https://i.imgur.com/2.jpg'); -INSERT INTO theme (name, description, thumbnail) VALUES ('어드벤처', '신나는 모험 테마', 'https://i.imgur.com/3.jpg'); - -INSERT INTO time (time) VALUES ('10:00'); -INSERT INTO time (time) VALUES ('13:00'); -INSERT INTO time (time) VALUES ('15:00'); - -INSERT INTO reservation (member_id, date, time_id, theme_id) VALUES (1, '2024-03-01', 1, 1); -INSERT INTO reservation (member_id, date, time_id, theme_id) VALUES (1, '2024-03-01', 2, 2); -INSERT INTO reservation (member_id, date, time_id, theme_id) VALUES (1, '2024-03-01', 3, 3); diff --git a/src/test/java/roomescape/JpaTest.java b/src/test/java/roomescape/JpaTest.java index 376567aed..f766a8237 100644 --- a/src/test/java/roomescape/JpaTest.java +++ b/src/test/java/roomescape/JpaTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; import roomescape.time.Time; diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 192cf1c5f..898b640e8 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -5,13 +5,13 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import org.junit.jupiter.api.BeforeEach; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import roomescape.reservation.dto.MyReservationResponse; import roomescape.reservation.dto.ReservationResponse; import roomescape.waiting.dto.WaitingResponse; @@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { @@ -48,39 +49,6 @@ private String createToken(String email, String password) { return response.response().getCookie("token"); } - @Autowired - private JdbcTemplate jdbcTemplate; - - @BeforeEach - void setUp() { - jdbcTemplate.execute("DELETE FROM waiting"); - jdbcTemplate.execute("DELETE FROM reservation"); - jdbcTemplate.execute("DELETE FROM member"); - jdbcTemplate.execute("DELETE FROM theme"); - jdbcTemplate.execute("DELETE FROM time"); - - jdbcTemplate.execute("ALTER TABLE waiting ALTER COLUMN id RESTART WITH 1"); - jdbcTemplate.execute("ALTER TABLE reservation ALTER COLUMN id RESTART WITH 1"); - jdbcTemplate.execute("ALTER TABLE member ALTER COLUMN id RESTART WITH 1"); - jdbcTemplate.execute("ALTER TABLE theme ALTER COLUMN id RESTART WITH 1"); - jdbcTemplate.execute("ALTER TABLE time ALTER COLUMN id RESTART WITH 1"); - - // Member (ID: 1=어드민, 2=브라운) - jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('어드민', 'admin@email.com', 'password', 'ADMIN')"); - jdbcTemplate.update("INSERT INTO member (name, email, password, role) VALUES ('브라운', 'brown@email.com', 'password', 'USER')"); - - // Theme (ID: 1) - jdbcTemplate.update("INSERT INTO theme (name, description, thumbnail) VALUES ('공포 테마', '매우 무서운 테마입니다.', 'thumbnail.jpg')"); - - // Time (ID: 1) - jdbcTemplate.update("INSERT INTO time (time) VALUES ('10:00')"); - - // Reservation (어드민 계정으로 3개의 예약 생성 - 오단계 테스트용) - jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2025-01-01', 1, 1, 1)"); - jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2025-01-02', 1, 1, 1)"); - jdbcTemplate.update("INSERT INTO reservation (date, member_id, theme_id, time_id) VALUES ('2025-01-03', 1, 1, 1)"); - } - @Test void 일단계() { String token = createToken("admin@email.com", "password"); @@ -199,4 +167,12 @@ void setUp() { Component componentAnnotation = JwtUtils.class.getAnnotation(Component.class); assertThat(componentAnnotation).isNull(); } + + @Value("${roomescape.auth.jwt.secret}") + private String secretKey; + + @Test + void 팔단계() { + AssertionsForClassTypes.assertThat(secretKey).isNotBlank(); + } } From f1bd8d158d440968d8c011a743375b40093c678b Mon Sep 17 00:00:00 2001 From: Jiwoo Date: Wed, 16 Jul 2025 19:37:22 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20&=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/auth/JwtUtils.java | 18 ++++++++- .../auth/ui/LoginUserArgumentResolver.java | 40 ++++++++----------- .../java/roomescape/ExceptionController.java | 23 +++++++++-- .../roomescape/auth/AdminInterceptor.java | 21 ++-------- .../java/roomescape/auth/LoginController.java | 5 +-- .../reservation/ReservationController.java | 1 + src/main/resources/static/js/reservation.js | 4 +- src/test/java/roomescape/JpaTest.java | 1 - 8 files changed, 62 insertions(+), 51 deletions(-) diff --git a/src/main/java/auth/JwtUtils.java b/src/main/java/auth/JwtUtils.java index 7a11c0c26..89fdf7515 100644 --- a/src/main/java/auth/JwtUtils.java +++ b/src/main/java/auth/JwtUtils.java @@ -5,8 +5,12 @@ 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 { @@ -23,7 +27,7 @@ public String createToken(String id, String name, String email, String role) { claims.put("email", email); claims.put("role", role); Date now = new Date(); - Date validity = new Date(now.getTime() + 3600); + Date validity = new Date(now.getTime() + 3600000); return Jwts.builder() .setClaims(claims) @@ -44,4 +48,16 @@ public Claims getClaims(String token) { throw new IllegalArgumentException("유효하지 않은 토큰입니다."); } } + + 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); + } } diff --git a/src/main/java/auth/ui/LoginUserArgumentResolver.java b/src/main/java/auth/ui/LoginUserArgumentResolver.java index 2ad4887e7..4e8d2b258 100644 --- a/src/main/java/auth/ui/LoginUserArgumentResolver.java +++ b/src/main/java/auth/ui/LoginUserArgumentResolver.java @@ -4,17 +4,16 @@ import auth.annotation.Login; import auth.dto.LoginMember; import io.jsonwebtoken.Claims; -import jakarta.servlet.http.Cookie; +import io.jsonwebtoken.JwtException; import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.NotNull; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; - -import java.util.Arrays; -import java.util.Optional; +import roomescape.auth.exception.UnauthenticatedException; @Component public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver { @@ -32,31 +31,26 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); - Optional tokenCookie = findTokenCookie(request.getCookies()); - if (tokenCookie.isEmpty()) { - return null; - } - String token = tokenCookie.get().getValue(); + String token = jwtUtils.getTokenFromCookie(request); - Claims claims = jwtUtils.getClaims(token); - Long id = Long.parseLong(claims.getSubject()); - String name = claims.get("name", String.class); - String email = claims.get("email", String.class); - String role = claims.get("role", String.class); + if (token == null) { + throw new UnauthenticatedException("로그인 토큰이 존재하지 않습니다."); + } - return new LoginMember(id, name, email, role); - } + try { + Claims claims = jwtUtils.getClaims(token); + Long id = Long.parseLong(claims.getSubject()); + String name = claims.get("name", String.class); + String email = claims.get("email", String.class); + String role = claims.get("role", String.class); - private Optional findTokenCookie(Cookie[] cookies) { - if (cookies == null) { - return Optional.empty(); + return new LoginMember(id, name, email, role); + } catch (JwtException| IllegalArgumentException e) { + throw new UnauthenticatedException("유효하지 않은 로그인 토큰입니다.", e); } - return Arrays.stream(cookies) - .filter(cookie -> "token".equals(cookie.getName())) - .findFirst(); } } diff --git a/src/main/java/roomescape/ExceptionController.java b/src/main/java/roomescape/ExceptionController.java index 4e2450f9e..f904d6c3c 100644 --- a/src/main/java/roomescape/ExceptionController.java +++ b/src/main/java/roomescape/ExceptionController.java @@ -1,14 +1,31 @@ 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 handleRuntimeException(Exception e) { - e.printStackTrace(); - return ResponseEntity.badRequest().build(); + public ResponseEntity handleRuntimeException(Exception e) { + log.error("예상치 못한 예외가 발생했습니다.", e); + return ResponseEntity.internalServerError().body("서버 내부 오류가 발생했습니다."); + } + + @ExceptionHandler(UnauthenticatedException.class) + public ResponseEntity handleUnauthenticatedException(UnauthenticatedException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); } } diff --git a/src/main/java/roomescape/auth/AdminInterceptor.java b/src/main/java/roomescape/auth/AdminInterceptor.java index 191a3e2c0..d0dfdf84e 100644 --- a/src/main/java/roomescape/auth/AdminInterceptor.java +++ b/src/main/java/roomescape/auth/AdminInterceptor.java @@ -2,15 +2,12 @@ import auth.JwtUtils; import io.jsonwebtoken.Claims; -import jakarta.servlet.http.Cookie; 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 { @@ -21,16 +18,15 @@ public AdminInterceptor(JwtUtils jwtUtils) { } @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - Optional 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(); Claims claims = jwtUtils.getClaims(token); String role = claims.get("role", String.class); @@ -45,13 +41,4 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return true; } - - private Optional findTokenCookie(Cookie[] cookies) { - if (cookies == null) { - return Optional.empty(); - } - return Arrays.stream(cookies) - .filter(cookie -> "token".equals(cookie.getName())) - .findFirst(); - } } diff --git a/src/main/java/roomescape/auth/LoginController.java b/src/main/java/roomescape/auth/LoginController.java index 1d8afcecb..21d462931 100644 --- a/src/main/java/roomescape/auth/LoginController.java +++ b/src/main/java/roomescape/auth/LoginController.java @@ -52,10 +52,7 @@ public ResponseEntity logout(HttpServletResponse response) { } @GetMapping("/login/check") - public ResponseEntity checkLogin(LoginMember loginMember) { - if (loginMember == null) { - return ResponseEntity.status(401).build(); - } + public ResponseEntity checkLogin(@Login LoginMember loginMember) { return ResponseEntity.ok(new LoginCheckResponse(loginMember.getName())); } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index e555cf0ea..ff50e83dc 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -2,6 +2,7 @@ import auth.annotation.Login; import auth.dto.LoginMember; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/resources/static/js/reservation.js b/src/main/resources/static/js/reservation.js index 8eaf9e47e..0f73eebef 100644 --- a/src/main/resources/static/js/reservation.js +++ b/src/main/resources/static/js/reservation.js @@ -131,9 +131,9 @@ function saveRow(event) { const row = event.target.parentNode.parentNode; const nameInput = row.querySelector('input[type="text"]'); - const themeSelect = row.querySelector('select'); + const themeSelect = row.querySelector('#theme-select'); const dateInput = row.querySelector('input[type="date"]'); - const timeSelect = row.querySelector('select'); + const timeSelect = row.querySelector('#time-select'); const reservation = { name: nameInput.value, diff --git a/src/test/java/roomescape/JpaTest.java b/src/test/java/roomescape/JpaTest.java index f766a8237..376567aed 100644 --- a/src/test/java/roomescape/JpaTest.java +++ b/src/test/java/roomescape/JpaTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; import roomescape.time.Time; From 03389383c1bfc88e3ee5124d07e67c0992bfd20a Mon Sep 17 00:00:00 2001 From: JiwooKim <163404760+Ji-Woo-Kim@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:36:23 +0900 Subject: [PATCH 5/8] Create README.md --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..130319fac --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# 🔓 방탈출 예약 서비스 + +
+image +image +
+ +## 🚀 주요 기능 + +1. **로그인**: 가입한 이메일과 비밀번호로 로그인할 수 있습니다. + +2. **예약**: '예약' 페이지에서 원하는 날짜, 테마, 시간을 선택하고 '예약하기' 버튼을 누르면 예약이 완료됩니다. + +3. **예약 확인 및 취소**: '내 예약' 페이지에서 예약 내역을 확인하거나, 원치 않는 예약을 취소할 수 있습니다. + +4. **예약 대기 신청**: 예약 대기를 신청하면, 취소 자리가 생겼을 때 우선적으로 예약할 기회를 드립니다. From a7a70b0726124108dbd155cbf796167a2ee096d7 Mon Sep 17 00:00:00 2001 From: Jiwoo Date: Sun, 20 Jul 2025 18:03:35 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/roomescape/ExceptionController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/roomescape/ExceptionController.java b/src/main/java/roomescape/ExceptionController.java index f904d6c3c..8c0a692d3 100644 --- a/src/main/java/roomescape/ExceptionController.java +++ b/src/main/java/roomescape/ExceptionController.java @@ -21,11 +21,13 @@ public ResponseEntity handleRuntimeException(Exception e) { @ExceptionHandler(UnauthenticatedException.class) public ResponseEntity handleUnauthenticatedException(UnauthenticatedException e) { + log.error("인증되지 않은 요청입니다.", e); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + log.error("잘못된 요청: {}", e.getMessage(), e); return ResponseEntity.badRequest().body(e.getMessage()); } } From e573b78788f182fe917a89d9224e9831d42fb395 Mon Sep 17 00:00:00 2001 From: Jiwoo Date: Sun, 20 Jul 2025 20:02:52 +0900 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20JWT=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=EC=A6=9D/=EC=9D=B8=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20-=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=97=90=20=EA=B3=A0=EC=9C=A0=20=EC=8B=9D?= =?UTF-8?q?=EB=B3=84=EC=9E=90(id),=20=EC=97=AD=ED=95=A0(role)=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A7=8C=20=ED=8F=AC=ED=95=A8=20-=20LoginMember,=20Lo?= =?UTF-8?q?ginUserArgumentResolver=20roomescape=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/auth/JwtUtils.java | 14 +++++++++---- .../roomescape/auth/AdminInterceptor.java | 4 +--- .../java/roomescape/auth/LoginController.java | 4 ++-- .../auth}/LoginUserArgumentResolver.java | 20 +++++++++---------- .../auth/dto/LoginMember.java | 2 +- .../java/roomescape/config/WebConfig.java | 2 +- .../reservation/ReservationController.java | 3 +-- .../reservation/ReservationService.java | 2 +- .../roomescape/waiting/WaitingController.java | 2 +- .../roomescape/waiting/WaitingService.java | 2 +- 10 files changed, 29 insertions(+), 26 deletions(-) rename src/main/java/{auth/ui => roomescape/auth}/LoginUserArgumentResolver.java (75%) rename src/main/java/{ => roomescape}/auth/dto/LoginMember.java (88%) diff --git a/src/main/java/auth/JwtUtils.java b/src/main/java/auth/JwtUtils.java index 89fdf7515..45ed6d471 100644 --- a/src/main/java/auth/JwtUtils.java +++ b/src/main/java/auth/JwtUtils.java @@ -21,10 +21,8 @@ public JwtUtils(String secretKey) { this.key = new SecretKeySpec(secretKey.getBytes(), SignatureAlgorithm.HS256.getJcaName()); } - public String createToken(String id, String name, String email, String role) { + public String createToken(String id, String role) { Claims claims = Jwts.claims().setSubject(id); - claims.put("name", name); - claims.put("email", email); claims.put("role", role); Date now = new Date(); Date validity = new Date(now.getTime() + 3600000); @@ -37,7 +35,7 @@ public String createToken(String id, String name, String email, String role) { .compact(); } - public Claims getClaims(String token) { + private Claims getClaims(String token) { try { Jws claims = Jwts.parserBuilder() .setSigningKey(key) @@ -49,6 +47,14 @@ public Claims getClaims(String token) { } } + 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) { diff --git a/src/main/java/roomescape/auth/AdminInterceptor.java b/src/main/java/roomescape/auth/AdminInterceptor.java index d0dfdf84e..5c64339d2 100644 --- a/src/main/java/roomescape/auth/AdminInterceptor.java +++ b/src/main/java/roomescape/auth/AdminInterceptor.java @@ -1,7 +1,6 @@ package roomescape.auth; import auth.JwtUtils; -import io.jsonwebtoken.Claims; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; @@ -27,8 +26,7 @@ public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServl } try { - Claims claims = jwtUtils.getClaims(token); - String role = claims.get("role", String.class); + String role = jwtUtils.getRole(token); if (!"ADMIN".equals(role)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/src/main/java/roomescape/auth/LoginController.java b/src/main/java/roomescape/auth/LoginController.java index 21d462931..b726bb082 100644 --- a/src/main/java/roomescape/auth/LoginController.java +++ b/src/main/java/roomescape/auth/LoginController.java @@ -2,7 +2,7 @@ import auth.JwtUtils; import auth.annotation.Login; -import auth.dto.LoginMember; +import roomescape.auth.dto.LoginMember; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; @@ -30,7 +30,7 @@ public LoginController(MemberService memberService, JwtUtils jwtUtils) { public ResponseEntity login(@RequestBody LoginRequest request, HttpServletResponse response) { Member member = memberService.login(request); - String token = jwtUtils.createToken(String.valueOf(member.getId()), member.getName(), member.getEmail(), member.getRole()); + String token = jwtUtils.createToken(String.valueOf(member.getId()), member.getRole()); Cookie cookie = new Cookie("token", token); cookie.setHttpOnly(true); diff --git a/src/main/java/auth/ui/LoginUserArgumentResolver.java b/src/main/java/roomescape/auth/LoginUserArgumentResolver.java similarity index 75% rename from src/main/java/auth/ui/LoginUserArgumentResolver.java rename to src/main/java/roomescape/auth/LoginUserArgumentResolver.java index 4e8d2b258..aa488dc36 100644 --- a/src/main/java/auth/ui/LoginUserArgumentResolver.java +++ b/src/main/java/roomescape/auth/LoginUserArgumentResolver.java @@ -1,9 +1,8 @@ -package auth.ui; +package roomescape.auth; import auth.JwtUtils; import auth.annotation.Login; -import auth.dto.LoginMember; -import io.jsonwebtoken.Claims; +import roomescape.auth.dto.LoginMember; import io.jsonwebtoken.JwtException; import jakarta.servlet.http.HttpServletRequest; import org.jetbrains.annotations.NotNull; @@ -14,14 +13,18 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import roomescape.auth.exception.UnauthenticatedException; +import roomescape.member.Member; +import roomescape.member.MemberService; @Component public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver { private final JwtUtils jwtUtils; + private final MemberService memberService; - public LoginUserArgumentResolver(JwtUtils jwtUtils) { + public LoginUserArgumentResolver(JwtUtils jwtUtils, MemberService memberService) { this.jwtUtils = jwtUtils; + this.memberService = memberService; } @Override @@ -42,13 +45,10 @@ public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewCo } try { - Claims claims = jwtUtils.getClaims(token); - Long id = Long.parseLong(claims.getSubject()); - String name = claims.get("name", String.class); - String email = claims.get("email", String.class); - String role = claims.get("role", String.class); + Long memberId = Long.parseLong(jwtUtils.getSubject(token)); + Member member = memberService.findById(memberId); - return new LoginMember(id, name, email, role); + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); } catch (JwtException| IllegalArgumentException e) { throw new UnauthenticatedException("유효하지 않은 로그인 토큰입니다.", e); } diff --git a/src/main/java/auth/dto/LoginMember.java b/src/main/java/roomescape/auth/dto/LoginMember.java similarity index 88% rename from src/main/java/auth/dto/LoginMember.java rename to src/main/java/roomescape/auth/dto/LoginMember.java index 211c49df7..28c838fa1 100644 --- a/src/main/java/auth/dto/LoginMember.java +++ b/src/main/java/roomescape/auth/dto/LoginMember.java @@ -1,4 +1,4 @@ -package auth.dto; +package roomescape.auth.dto; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java index b8987a87e..7949d0afb 100644 --- a/src/main/java/roomescape/config/WebConfig.java +++ b/src/main/java/roomescape/config/WebConfig.java @@ -1,6 +1,6 @@ package roomescape.config; -import auth.ui.LoginUserArgumentResolver; +import roomescape.auth.LoginUserArgumentResolver; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index ff50e83dc..9ce38ffce 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -1,8 +1,7 @@ package roomescape.reservation; import auth.annotation.Login; -import auth.dto.LoginMember; -import org.springframework.http.HttpStatus; +import roomescape.auth.dto.LoginMember; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index 1690e3095..7d15c6256 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,6 +1,6 @@ package roomescape.reservation; -import auth.dto.LoginMember; +import roomescape.auth.dto.LoginMember; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.member.Member; diff --git a/src/main/java/roomescape/waiting/WaitingController.java b/src/main/java/roomescape/waiting/WaitingController.java index 027e18a7c..f5373f61e 100644 --- a/src/main/java/roomescape/waiting/WaitingController.java +++ b/src/main/java/roomescape/waiting/WaitingController.java @@ -1,7 +1,7 @@ package roomescape.waiting; import auth.annotation.Login; -import auth.dto.LoginMember; +import roomescape.auth.dto.LoginMember; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; diff --git a/src/main/java/roomescape/waiting/WaitingService.java b/src/main/java/roomescape/waiting/WaitingService.java index 664484849..8e91d74cd 100644 --- a/src/main/java/roomescape/waiting/WaitingService.java +++ b/src/main/java/roomescape/waiting/WaitingService.java @@ -1,7 +1,7 @@ package roomescape.waiting; import auth.annotation.Login; -import auth.dto.LoginMember; +import roomescape.auth.dto.LoginMember; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.auth.exception.UnauthenticatedException; From 716da1f3f9e9407cd041141d5e6d767b84053883 Mon Sep 17 00:00:00 2001 From: Jiwoo Date: Thu, 24 Jul 2025 15:20:56 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20AuthConfig=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/{auth => roomescape/config}/AuthConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename src/main/java/{auth => roomescape/config}/AuthConfig.java (88%) diff --git a/src/main/java/auth/AuthConfig.java b/src/main/java/roomescape/config/AuthConfig.java similarity index 88% rename from src/main/java/auth/AuthConfig.java rename to src/main/java/roomescape/config/AuthConfig.java index 27ecdc67e..09abb6b06 100644 --- a/src/main/java/auth/AuthConfig.java +++ b/src/main/java/roomescape/config/AuthConfig.java @@ -1,5 +1,6 @@ -package auth; +package roomescape.config; +import auth.JwtUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;