Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
89c3a69
feat: 토큰 생성 및 검증 기능 추가
mgim9316-a11y Jun 29, 2026
1e6f4cf
feat: 로그인 API 구현
mgim9316-a11y Jun 29, 2026
c348327
feat: 서버의 토큰 발급 확인 테스트 코드
mgim9316-a11y Jun 29, 2026
29152d3
refactor: resorver가 토큰을 받는 형태로 수정
mgim9316-a11y Jun 29, 2026
f084866
refactor: resorver가 토큰을 받는 형태로 수정
mgim9316-a11y Jun 29, 2026
da4f4ec
refactor: 예약 생성 시 ReservationReqeust의 name이 없는 경우 Cookie에 담긴 정보를 활용
mgim9316-a11y Jun 29, 2026
0853489
fix: 2단계 테스트 코드 오류 수정
mgim9316-a11y Jun 29, 2026
40bb963
커밋테스트
mgim9316-a11y Jun 30, 2026
0c8710c
커밋테스트
mgim9316-a11y Jun 30, 2026
0a5c76b
커밋테스트
mgim9316-a11y Jun 30, 2026
cdaddb5
커밋테스트
mgim9316-a11y Jun 30, 2026
50c51c3
커밋테스트
hapdaypy Jun 30, 2026
76ce709
refactor: JWT 생성/검증 책임 클래스 분리
hapdaypy Jul 3, 2026
5b27027
refactor: 네이밍 수정 및 로그인한 회원의 정보를 담는 객체 분리
hapdaypy Jul 3, 2026
7479e06
refactor: 네이밍 수정 및 로그인한 회원의 정보를 담는 객체 분리
hapdaypy Jul 3, 2026
bd2b014
refactor: 쿠키 관련 로직을 분리
hapdaypy Jul 3, 2026
4aff64a
refactor: simpledbcInsert로 변경
hapdaypy Jul 3, 2026
b2e568f
refactor: ReservationDao의 조건 조회를 NamedParameterJdbcTemplate 로 바꿈
hapdaypy Jul 3, 2026
66ce159
feat: “내 예약” 화면이 호출하는 API
hapdaypy Jul 3, 2026
506eceb
feat: 공통 mapper 메서드를 추가
hapdaypy Jul 3, 2026
fbadb3f
refactor: 로그인 실패 조건식 수정
hapdaypy Jul 3, 2026
446bd84
feat: 에러 응답 메시지 추가
hapdaypy Jul 3, 2026
5870f17
refactor: 애노테이션을 옵션으로 분기하지 않고, Optional로 선택 가능성 표현
hapdaypy Jul 3, 2026
a862274
refactor: 인증 패키지 분리
hapdaypy Jul 6, 2026
d3c5540
refactor: 가독성을 위해 코드 분리
hapdaypy Jul 6, 2026
68ab241
refactor: AuthUser, AdminOnly 권한 분리
hapdaypy Jul 6, 2026
765d7a4
refactor: 인텁센터 로직 수정
hapdaypy Jul 6, 2026
b207c49
feat: 예외 처리를 상태코드로 나눔
hapdaypy Jul 7, 2026
ce3992d
refactor: DB 조회 데이터가 0개일 경우 에러를 NotFoundException 로 반환
hapdaypy Jul 7, 2026
cc91e2a
feat: 생성 멱등성을 검증하는 로직 추가
hapdaypy Jul 7, 2026
dbf7630
feat: 일반 사용자 기능과 관리자 기능을 인텁셉터를 도입하여 분리
hapdaypy Jul 7, 2026
6a6e810
refactor: controller의 예외 처리 로직을 service로 반경
hapdaypy Jul 12, 2026
da6a387
refactor: jjwt의 JwtParser 사용
hapdaypy Jul 12, 2026
654fff7
refactor: 사용자의 role을 enum으로 관리
hapdaypy Jul 12, 2026
319ac52
feat: JWT access/refresh token 발급 흐름 추가
hapdaypy Jul 12, 2026
c9f3185
refactor: DTO 성격을 record 형태로 변경 및 record 호출 형태로 수정
hapdaypy Jul 12, 2026
24416b3
refactor: raw type 을 구체적 타입으로 수정
hapdaypy Jul 12, 2026
bace53d
refactor: record 형태로 수정
hapdaypy Jul 12, 2026
87aca69
fix: cherry-pick conflict cleanup
hapdaypy Jul 12, 2026
41e1d3a
refactor:수동 getter를 @Getter 로 정리
hapdaypy Jul 13, 2026
8374a94
feat: @Transactional 사용
hapdaypy Jul 13, 2026
8c10705
feat: @Entity로 바꿈
hapdaypy Jul 13, 2026
737af23
refactor: EntityManager 기반으로 교체
hapdaypy Jul 13, 2026
f6b104e
feat: 테마 기능 서비스 생성
hapdaypy Jul 13, 2026
74b7d5e
refactor: 컨트롤러에서 DAO, 서비스 의존성 제거
hapdaypy Jul 13, 2026
8aebdb7
refactor: optional 추가
hapdaypy Jul 13, 2026
34599e8
refactor: 예약과 회원을 JPA 연관관계로 매핑
hapdaypy Jul 13, 2026
e0ac584
refactor: 상속하는 파일들 패키지로 분리
hapdaypy Jul 14, 2026
e0bca6d
refactor: 에러메시지를 enum으로 관리
hapdaypy Jul 14, 2026
8a2b3fd
refactor: 사용되지 않은 생성자 삭제
hapdaypy Jul 16, 2026
2387c0f
refactor: RuntimeException 전체를 잡아 AuthenticationException으로 변환하는 부분 수정
hapdaypy Jul 18, 2026
45bb709
feat: 토큰 파싱 실패시, AuthenticationException으로 반환
hapdaypy Jul 18, 2026
815c598
refactor: @Value로 secret 주입받아 key를 만들도록 변경
hapdaypy Jul 18, 2026
d914306
refactor: isValidSignature()를 제거
hapdaypy Jul 18, 2026
255dcae
feat: 이메일 컬럼 매핑 추가
hapdaypy Jul 18, 2026
0c00db4
refactor: getSingleResult()에서 발생하는 예외 사용
hapdaypy Jul 18, 2026
f764dd3
refactor: 회원이 없을 때 예외를 던지지 않고Optional<Member>를 반환
hapdaypy Jul 18, 2026
6759fac
refactor: 서비스에서 실제 엔티티를 조회한 뒤 DAO에 넘기도록 변경
hapdaypy Jul 18, 2026
b45a1b0
refactor: join과 DTO projection 을 사용하도록 리팩토링
hapdaypy Jul 18, 2026
39fa068
refactor: id를 받는 생성자 삭제
hapdaypy Jul 18, 2026
c6d7687
merge: sync hapdaypy base
hapdaypy Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ 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'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'

Expand Down
4 changes: 0 additions & 4 deletions src/main/java/roomescape/AuthenticationException.java

This file was deleted.

64 changes: 50 additions & 14 deletions src/main/java/roomescape/ExceptionController.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,68 @@
package roomescape;

import lombok.Getter;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import roomescape.exception.AuthenticationException;
import roomescape.exception.AuthorizationException;
import roomescape.exception.ConflictException;
import roomescape.exception.ErrorCode;
import roomescape.exception.NotFoundException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
@ControllerAdvice // 아 컨트롤러에서 발생한 예외를 가로챔 ~ ㅋ
public class ExceptionController {
private static final Logger log = LoggerFactory.getLogger(ExceptionController.class);

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthenticationException(AuthenticationException e) {
return ResponseEntity.status(401)
.body(new ErrorResponse(401, "로그인이 필요합니다."));
return error(e.getErrorCode());
}

@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<ErrorResponse> handleAuthorizationException(AuthorizationException e) {
return error(e.getErrorCode());
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(NotFoundException e) {
log.warn("Resource not found: {}", e.getMessage());
return error(e.getErrorCode());
}

@ExceptionHandler({ConflictException.class, DataIntegrityViolationException.class})
public ResponseEntity<ErrorResponse> handleConflictException(Exception e) {
if (e instanceof ConflictException conflictException) {
return error(conflictException.getErrorCode());
}
return error(ErrorCode.CONFLICT_REQUEST);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) {
return error(HttpStatus.BAD_REQUEST, e.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(Exception e) {
public ResponseEntity<ErrorResponse> handleException(Exception e) {
e.printStackTrace();
return ResponseEntity.badRequest()
.body(new ErrorResponse(400, "잘못된 요청입니다."));
return error(ErrorCode.INTERNAL_SERVER_ERROR);
}

private ResponseEntity<ErrorResponse> error(ErrorCode errorCode) {
return error(errorCode.getStatus(), errorCode.getMessage());
}

private ResponseEntity<ErrorResponse> error(HttpStatus status, String message) {
return ResponseEntity.status(status)
.body(new ErrorResponse(status.value(), message));
}

@Getter
public static class ErrorResponse {
private final int statusCode;
private final String message;
Expand All @@ -27,13 +71,5 @@ public ErrorResponse(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}

public int getStatusCode() {
return statusCode;
}

public String getMessage() {
return message;
}
}
}
5 changes: 5 additions & 0 deletions src/main/java/roomescape/PageController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import roomescape.auth.AdminOnly;

@Controller
public class PageController {
@GetMapping("/admin")
@AdminOnly
public String admin() {
return "admin/index";
}

@GetMapping("/admin/reservation")
@AdminOnly
public String adminReservation() {
return "admin/reservation";
}

@GetMapping("/admin/theme")
@AdminOnly
public String adminTheme() {
return "admin/theme";
}

@GetMapping("/admin/time")
@AdminOnly
public String adminTime() {
return "admin/time";
}
Expand Down
22 changes: 15 additions & 7 deletions src/main/java/roomescape/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,30 @@

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.LoginMemberArgumentResolver;
import roomescape.auth.AdminInterceptor;
import roomescape.auth.AuthUserArgumentResolver;

import java.util.List;

@Configuration
@Configuration // Spring MVC 설정을 추가함
public class WebConfig implements WebMvcConfigurer {
private final LoginMemberArgumentResolver loginMemberArgumentResolver;
private final AuthUserArgumentResolver authUserArgumentResolver;
private final AdminInterceptor adminInterceptor;

public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) {
this.loginMemberArgumentResolver = loginMemberArgumentResolver;
public WebConfig(AuthUserArgumentResolver authUserArgumentResolver, AdminInterceptor adminInterceptor) {
this.authUserArgumentResolver = authUserArgumentResolver;
this.adminInterceptor = adminInterceptor;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginMemberArgumentResolver);
// 로그인에 이 resolver를 사용해라
resolvers.add(authUserArgumentResolver);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminInterceptor);
}
}
40 changes: 40 additions & 0 deletions src/main/java/roomescape/auth/AdminInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package roomescape.auth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.exception.AuthorizationException;
import roomescape.member.MemberService;
import roomescape.member.MemberRole;

@Component
public class AdminInterceptor implements HandlerInterceptor {
private final AuthCookieProvider authCookieProvider;
private final MemberService memberService;

public AdminInterceptor(AuthCookieProvider authCookieProvider, MemberService memberService) {
this.authCookieProvider = authCookieProvider;
this.memberService = memberService;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod handlerMethod) || !hasAdminOnly(handlerMethod)) {
return true;
}

String token = authCookieProvider.extractAccessToken(request);
LoginMemberInfo loginMember = memberService.checkLogin(token);
if (MemberRole.ADMIN == loginMember.role()) {
return true;
}
throw new AuthorizationException();
}

private boolean hasAdminOnly(HandlerMethod handlerMethod) {
return handlerMethod.hasMethodAnnotation(AdminOnly.class)
|| handlerMethod.getBeanType().isAnnotationPresent(AdminOnly.class);
}
}
11 changes: 11 additions & 0 deletions src/main/java/roomescape/auth/AdminOnly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package roomescape.auth;

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

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminOnly {
}
57 changes: 52 additions & 5 deletions src/main/java/roomescape/auth/AuthCookieProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,86 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import roomescape.AuthenticationException;
import roomescape.exception.AuthenticationException;

@Component
public class AuthCookieProvider {
private static final String TOKEN_COOKIE_NAME = "token";
private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";
private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";
private static final String LEGACY_TOKEN_COOKIE_NAME = "token";
private static final String COOKIE_PATH = "/";

public Cookie createLoginCookie(String token) {
Cookie cookie = new Cookie(TOKEN_COOKIE_NAME, token);
return createAccessTokenCookie(token);
}

public Cookie createAccessTokenCookie(String token) {
return createCookie(ACCESS_TOKEN_COOKIE_NAME, token);
}

public Cookie createRefreshTokenCookie(String token) {
return createCookie(REFRESH_TOKEN_COOKIE_NAME, token);
}

private Cookie createCookie(String name, String value) {
Cookie cookie = new Cookie(name, value);
cookie.setHttpOnly(true);
cookie.setPath(COOKIE_PATH);
return cookie;
}

public Cookie createLogoutCookie() {
Cookie cookie = new Cookie(TOKEN_COOKIE_NAME, "");
return createLogoutCookie(LEGACY_TOKEN_COOKIE_NAME);
}

public Cookie createLogoutAccessTokenCookie() {
return createLogoutCookie(ACCESS_TOKEN_COOKIE_NAME);
}

public Cookie createLogoutRefreshTokenCookie() {
return createLogoutCookie(REFRESH_TOKEN_COOKIE_NAME);
}

private Cookie createLogoutCookie(String name) {
Cookie cookie = new Cookie(name, "");
cookie.setHttpOnly(true);
cookie.setPath(COOKIE_PATH);
cookie.setMaxAge(0);
return cookie;
}

public String extractToken(HttpServletRequest request) {
return extractAccessToken(request);
}

public String extractAccessToken(HttpServletRequest request) {
return extractToken(request, ACCESS_TOKEN_COOKIE_NAME, LEGACY_TOKEN_COOKIE_NAME);
}

public String extractRefreshToken(HttpServletRequest request) {
return extractToken(request, REFRESH_TOKEN_COOKIE_NAME);
}

private String extractToken(HttpServletRequest request, String... cookieNames) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
throw new AuthenticationException();
}

for (Cookie cookie : cookies) {
if (TOKEN_COOKIE_NAME.equals(cookie.getName())) {
if (contains(cookieNames, cookie.getName())) {
return cookie.getValue();
}
}
throw new AuthenticationException();
}

private boolean contains(String[] values, String target) {
for (String value : values) {
if (value.equals(target)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginMember {
public @interface AuthUser {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import roomescape.AuthenticationException;
import roomescape.exception.AuthenticationException;
import roomescape.member.MemberService;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Optional;

@Component
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {
private final MemberService memberService;
private final AuthCookieProvider authCookieProvider;

public LoginMemberArgumentResolver(MemberService memberService, AuthCookieProvider authCookieProvider) {
public AuthUserArgumentResolver(MemberService memberService, AuthCookieProvider authCookieProvider) {
this.memberService = memberService;
this.authCookieProvider = authCookieProvider;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginMember.class)
return parameter.hasParameterAnnotation(AuthUser.class)
&& (LoginMemberInfo.class.isAssignableFrom(parameter.getParameterType())
|| isOptionalLoginMemberInfo(parameter));
}
Expand All @@ -46,13 +46,13 @@ public Object resolveArgument(
}

try {
String token = authCookieProvider.extractToken(request);
String token = authCookieProvider.extractAccessToken(request);
LoginMemberInfo loginMember = memberService.checkLogin(token);
if (optional) {
return Optional.of(loginMember);
}
return loginMember;
} catch (RuntimeException e) {
} catch (AuthenticationException e) {
return handleAuthenticationFailure(optional);
}
}
Expand Down
Loading